Side B fixes a real user-facing bug (empty /-/ external index due to querying a bogus parent), adds a coherent resolvers/ module with a typed GithubImportCard schema and rendering, plus CSS/layout and integration tests validating the fix. Side A is a substantial architectural rework of URL canonicalization (composable url_rules engine) which is well-designed but is largely a refactor/rename churn (test-string updates dominate the diff) without fixing an observed bug, making B's concrete correctness fix and shippable feature slightly more valuable.
constitution · epochs · watch · epoch 3
c_77729db919ab (tommy-mor) vs c_df12ba3b70a8 (tommy-mor)
download prompt · raw event · cmp_487a0a1f4d78bb
council reasoning
Side A introduces a real composable URL canonicalization subsystem (url_rules engine + registry), migrates ItemId to full https URLs, and rewires parent/breadcrumb/storage paths across the core model—foundational identity design with lasting project impact. Side B fixes one broken /- index query and upgrades GitHub import presentation (structured cards, resolvers/ move, CSS), which is valuable product work but narrower and partly UI/reorg relative to A’s identity layer.
Side A makes a foundational change to the project's identity model by introducing a composable URL canonicalization subsystem (`server/src/url_rules/`), switching `ItemId` to canonical full URLs, centralizing parent/breadcrumb logic, and updating projection/event parsing so stored IDs are normalized consistently. Side B delivers a real bug fix for the external garden index and improves GitHub resolver rendering with rich cards, but most of its changes are feature/UI-oriented and a module reorganization rather than a core architectural improvement.
Side A introduces a full URL canonicalization system (`url_rules` with parsing, normalization, breadcrumbs, parent logic) and rewires `ItemId` to use it, replacing ad-hoc logic across the codebase—this is a foundational design improvement. Side B fixes an index bug (removing bogus `https://.` parent) and adds GitHub resolver rendering/cards, but these are more feature/UI-oriented and less structurally pervasive than A’s core identity model overhaul.
Side A introduces a new `url_rules` engine and refactors `ItemId` to use canonical full URLs (e.g., `https://reddit.com/...`) across parsing, storage, parents, breadcrumbs, projection, and Reddit integration—removing the old ad-hoc `canonicalize` logic and centralizing normalization. This is a foundational identity-layer redesign affecting many modules and tests. Side B adds a valuable GitHub resolver module and fixes the external garden index, but it builds on existing structures rather than reshaping core URL identity semantics.
sides
A — c_77729db919ab (tommy-mor)
message
[239c074b] url schema stuff
diff preview
diff --git a/AGENTS.md b/AGENTS.md
index 426a88e7c1da54fe0a28c5c76fa4e1f1bc117fcf..e60b9ba6012593361ef10e8fdd9439cd9932e09b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -58,3 +58,4 @@ Use **tmux** for `cargo run --package sorter2-server` (dev server). Rebuild afte
- First `cargo test` / `cargo build --release` is slow; Clojure smoke test always does a release build.
- `legacy/` and `ideas/` are not part of the workspace build.
+- **ItemId** for web URLs is a canonical full URL (`https://reddit.com/r/rust`). Rules live in [`server/src/url_rules/`](server/src/url_rules/) (composable Rust, not a config DSL). After changing canonicalization rules, rebuild the projection: `cargo run --package sorter2-server -- replay-index`.
diff --git a/Cargo.lock b/Cargo.lock
index 0dd4fce5fb6400ae153cca4e3dbf5a5158e6d8b4..49a908ef935c430dbe63c6a28d8a24e38b489486 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1951,6 +1951,7 @@ dependencies = [
"tower-http 0.5.2",
"tracing",
"tracing-subscriber",
+ "url",
"urlencoding",
]
diff --git a/REPLAY.sh b/REPLAY.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f2dbd8aea60c02d2feef74805f7ef5c2b7022537
--- /dev/null
+++ b/REPLAY.sh
@@ -0,0 +1,2 @@
+cargo run --package sorter2-server -- replay-index
+
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 27f552c20b97ef28cdde4cb6b1a4980375135111..ad4912791aff59fb1d3293f66ad381ae618cd60b 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -24,6 +24,7 @@ async-stream = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
rand = "0.8"
urlencoding = "2"
+url = "2"
durable = { path = "../durable" }
[dev-dependencies]
diff --git a/server/src/entity_store.rs b/server/src/entity_store.rs
index d5d17c3676e4a8ddec998e9f5a9dbafe9c2d9d0e..d29f39aecca6f12cdcf263cf77c3654eb4ee6cfa 100644
--- a/server/src/entity_store.rs
+++ b/server/src/entity_store.rs
@@ -124,7 +124,7 @@ mod tests {
fn round_trip_payload() {
let tmp = tempfile::tempdir().unwrap();
let store = EntityStore::open(tmp.path()).unwrap();
- let id = ItemId::parse("reddit.com/r/rust").unwrap();
+ let id = ItemId::from_url("https://reddit.com/r/rust").unwrap();
let payload = json!({"kind": "t5", "data": {"display_name": "rust"}});
store.put(&id, &payload).unwrap();
diff --git a/server/src/event_log.rs b/server/src/event_log.rs
index 36f5b406084065b608735987cdb483c236e03081..2c9290b6fdbf2c2ad1c0f1ffd7374b2d9cc97f36 100644
--- a/server/src/event_log.rs
+++ b/server/src/event_log.rs
@@ -199,7 +199,7 @@ mod tests {
log.append(&sample_record(
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
))
.await
@@ -237,7 +237,7 @@ mod tests {
let path = tmp.path().join("events.jsonl");
let log = EventLog::new(&path);
let event = Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
};
log.append(&sample_record(1, event)).await.unwrap();
@@ -255,7 +255,7 @@ mod tests {
let path = tmp.path().join("events.jsonl");
std::fs::write(
&path,
- r#"{"type":"node_ensured","id":"reddit.com/r/rust"}
+ r#"{"type":"node_ensured","id":"https://reddit.com/r/rust"}
{"schema":1,"seq":1,"ts":1,"event":{"type":"vote_recorded","ts":1,"a":"a","b":"b","ratio_left":2,"ratio_right":1,"scope":""}}
"#,
)
@@ -295,7 +295,7 @@ mod tests {
log.append(&sample_record(
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
))
.await
@@ -303,7 +303,7 @@ mod tests {
log.append(&sample_record(
3,
Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
},
))
.await
diff --git a/server/src/journal.rs b/server/src/journal.rs
index 521a108019de1ea870d14c4fafbfe572c20ce0de..50bc89f976edb82b7b0e49e954a8eccbbe82bf87 100644
--- a/server/src/journal.rs
+++ b/server/src/journal.rs
@@ -141,10 +141,10 @@ mod tests {
let j2 = journal.clone();
let (r1, r2) = tokio::join!(
j1.append(Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
}),
j2.append(Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
}),
);
r1.unwrap();
@@ -153,10 +153,10 @@ mod tests {
assert_eq!(projection_store.last_applied_event_count().unwrap(), 2);
let tree = projection_store.load_tree().unwrap();
assert!(tree
- .get(&ItemId::parse("reddit.com/r/rust").unwrap())
+ .get(&ItemId::parse("https://reddit.com/r/rust").unwrap())
.is_some());
assert!(tree
- .get(&ItemId::parse("reddit.com/r/python").unwrap())
+ .get(&ItemId::parse("https://reddit.com/r/python").unwrap())
.is_some());
}
@@ -170,7 +170,7 @@ mod tests {
1,
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
))
.await
@@ -186,7 +186,7 @@ mod tests {
1,
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
)],
)
@@ -202,7 +202,7 @@ mod tests {
);
journal
.append(Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
})
.await
.unwrap();
@@ -227,13 +227,13 @@ mod tests {
journal
.append_many(vec![
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
},
Event::NodeEnsured {
- id: "reddit.com/r/clojure".into(),
+ id: "https://reddit.com/r/clojure".into(),
},
])
.await
@@ -245,7 +245,7 @@ mod tests {
assert_eq!(projection_store.last_applied_event_count().unwrap(), 3);
let tree = projection_store.load_tree().unwrap();
assert!(tree
- .get(&ItemId::parse("reddit.com/r/clojure").unwrap())
+ .get(&ItemId::parse("https://reddit.com/r/clojure").unwrap())
.is_some());
}
}
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 9bd5f76fd1406b9b1be4c272f4ba8647edde2678..5c02c8e704e4664453bad75d819df8a067668176 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -9,6 +9,7 @@ pub mod journal;
pub mod pair;
pub mod parser;
pub mod path_types;
+pub mod url_rules;
pub mod projection_apply;
pub mod projection_store;
pub mod ranking;
diff --git a/server/src/pair.rs b/server/src/pair.rs
index 43f780ba6ea6ce1cdc2e1f4cbb252ba8a10684b9..815a97b80e3e9f348e0937a4f147f2862018edb0 100644
--- a/server/src/pair.rs
+++ b/server/src/pair.rs
@@ -381,42 +381,42 @@ mod tests {
#[test]
fn suggest_prefers_unvoted_pair() {
- let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+ let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
let mut tree = seed_children(
&parent,
&[
- "reddit.com/r/rust/a",
- "reddit.com/r/rust/b",
- "reddit.com/r/rust/c",
+ "https://reddit.com/r/rust/a",
+ "https://reddit.com/r/rust/b",
+ "https://reddit.com/r/rust/c",
],
);
let vote =
- VoteData::from_recorded(1, "reddit.com/r/rust/a", "reddit.com/r/rust/b", 2, 1).unwrap();
+ VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
tree.apply_vote(&parent, vote);
let group = tree.get(&parent).unwrap().local_ranking.clone();
let pool = children_of(&tree, &parent);
let (l, r) = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
- let voted_ab = (l.as_str() == "reddit.com/r/rust/a" && r.as_str() == "reddit.com/r/rust/b")
- || (l.as_str() == "reddit.com/r/rust/b" && r.as_str() == "reddit.com/r/rust/a");
+ let voted_ab = (l.as_str() == "https://reddit.com/r/rust/a" && r.as_str() == "https://reddit.com/r/rust/b")
+ || (l.as_str() == "https://reddit.com/r/rust/b" && r.as_str() == "https://reddit.com/r/rust/a");
assert!(!voted_ab);
}
#[test]
fn suggest_bridges_separate_components() {
- let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+ let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
let mut tree = seed_children(
&parent,
&[
- "reddit.com/r/rust/a",
- "reddit.com/r/rust/b",
- "reddit.com/r/rust/c",
- "reddit.com/r/rust/d",
+ "https://reddit.com/r/rust/a",
+ "https://reddit.com/r/rust/b",
+ "https://reddit.com/r/rust/c",
+ "https://reddit.com/r/rust/d",
],
);
let ab =
- VoteData::from_recorded(1, "reddit.com/r/rust/a", "reddit.com/r/rust/b", 2, 1).unwrap();
+ VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
let cd =
- VoteData::from_recorded(2, "reddit.com/r/rust/c", "reddit.com/r/rust/d", 2, 1).unwrap();
+ VoteData::from_recorded(2, "https://reddit.com/r/rust/c", "https://reddit.com/r/rust/d", 2, 1).unwrap();
tree.apply_vote(&parent, ab);
tree.apply_vote(&parent, cd);
let group = tree.get(&parent).unwrap().local_ranking.clone();
@@ -424,37 +424,37 @@ mod tests {
let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
let chosen = pair_set(&pair);
let from_ab =
- chosen.contains("reddit.com/r/rust/a") || chosen.contains("reddit.com/r/rust/b");
+ chosen.contains("https://reddit.com/r/rust/a") || chosen.contains("https://reddit.com/r/rust/b");
let from_cd =
- chosen.contains("reddit.com/r/rust/c") || chosen.contains("reddit.com/r/rust/d");
+ chosen.contains("https://reddit.com/r/rust/c") || chosen.contains("https://reddit.com/r/rust/d");
assert!(from_ab && from_cd, "expected bridge pair, got {:?}", chosen);
}
#[test]
fn suggest_prefers_attach_over_isolate_pair_among_many_unranked() {
- let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+ let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
let mut tree = seed_children(
&parent,
&[
- "reddit.com/r/rust/a",
- "reddit.com/r/rust/b",
- "reddit.com/r/rust/c",
- "reddit.com/r/rust/d",
- "reddit.com/r/rust/e",
+ "https://reddit.com/r/rust/a",
+ "https://reddit.com/r/rust/b",
+ "https://reddit.com/r/rust/c",
+ "https://reddit.com/r/
… preview truncated; 51,799 characters omittedB — c_df12ba3b70a8 (tommy-mor)
message
[23c8134e] Fix /-/ external garden index; resolvers/ + GitHub import cards (#150) * Fix external garden root listing; add resolvers/ with GitHub cards The public and room external index pages queried children of a bogus https://./ parent, so /-/ always looked empty. Collect host-only https roots from all Web items and item_children edges so ghost parents from add_child_edge appear. Move GitHub resolver into server/src/resolvers/ with default_external.rs and a try_render_resolver_item_body hook. Resolver ingests now store slug-github-card fenced JSON; render_item_body_in_scope shows a small GitHub article card (with legacy support for schema-less json fences on github.com URLs). Styling in theme_default.css; agents.md updated. Co-authored-by: tommy <thmorriss@gmail.com> * Vote compare: GitHub cards in columns, layout CSS, tests Pass item_bodies into vote_compare_item_card for linkified tooltips on non-card bodies; clone item_bodies before dropping reducer read guard. Add layout rules so rich cards sit in the grid corners (default + retro). Unit test on vote_compare_item_card; integration GET /vote/compare with ingested slug-github-card bodies. agents.md clarifies compare columns. Co-authored-by: tommy <thmorriss@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
diff preview
diff --git a/agents.md b/agents.md
index 7508234d9b04223d0e64cfe69fedbebd06a256b5..d8b801e454fdf37e7ac6038b91a69f83b0746d59 100644
--- a/agents.md
+++ b/agents.md
@@ -42,7 +42,7 @@ Strict **CSP** that blocks `eval` would break the current app. Other projects ma
- **`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.
-- **`ResolveExternal`:** GitHub resolver buttons are browser actions through **`POST /ui`**. Success responses morph **`#external-resolver-status`** then redirect to the sanitized shareable **`GET`** page so imported children render through the normal page path; errors morph the same status region. Resolver results are durable system ingests, while cooldown state is RAM-only.
+- **`ResolveExternal`:** GitHub resolver buttons are browser actions through **`POST /ui`**. Success responses morph **`#external-resolver-status`** then redirect to the sanitized shareable **`GET`** page so imported children render through the normal page path; errors morph the same status region. Resolver results are durable system ingests, while cooldown state is RAM-only. Implementation lives under **`server/src/resolvers/`** (GitHub resolver + import card JSON); ontology item pages and the **`GET /vote/compare`** left/right columns use **`render_item_body_in_scope`** in **`server/src/html/mod.rs`**, which calls **`server/src/resolvers/mod.rs::try_render_resolver_item_body`** before falling back to the usual **`<pre>`** linkified view.
- **Garden pin / compare voting:** Cookie **`slug_garden_pin`** via **`set_garden_pin`**. Pairwise UI: **`GET /vote/compare?…`** / **`GET /r/:room_key/vote/compare?…`** (fullscreen **`GET`** page: no HUD; other garden pages). HUD (**`#slug-pin-hud`**): only when **`layout`** passes garden metadata on **`body`**; the label is **`POST /ui`** **`set_garden_pin`** **`clear:true`** (**`slug_ui.js`**), not a permalink to the item.
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 4b0214d18b173cd506d09176104f461dc4c4f208..c9eb8e242072e41fcf70da838bdf02dd4c838db8 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -18,7 +18,7 @@ use crate::{
rpc::{rpc_post_redact, rpc_post_with_bearer, rpc_room_delete},
},
canonical_path::canonicalize_tag,
- external_resolver::resolve_github_children,
+ resolvers::resolve_github_children,
html::vote_compare_post_success_js,
html::{
external_resolver_status_markup, fragment_new_thread_slot, login_to_post_hint_markup,
diff --git a/server/src/external_resolver.rs b/server/src/external_resolver.rs
deleted file mode 100644
index a5812250fed7613950b5417f396f886a55fafccf..0000000000000000000000000000000000000000
--- a/server/src/external_resolver.rs
+++ /dev/null
@@ -1,630 +0,0 @@
-use async_trait::async_trait;
-use serde_json::Value;
-use tokio::sync::oneshot;
-
-use crate::{path_types::ItemId, state::AppState, write_cmd::WriteCmd};
-
-const GITHUB_SYSTEM_PRINCIPAL: &str = "system:github-resolver";
-const GITHUB_RESOLVER_COOLDOWN_MS: i64 = 15_000;
-const GITHUB_MAX_PAGES: usize = 3;
-
-fn now_ms() -> i64 {
- use std::time::{SystemTime, UNIX_EPOCH};
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or_default()
- .as_millis() as i64
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResolvedChild {
- pub url: String,
- pub title: String,
- pub body: Option<String>,
-}
-
-#[async_trait]
-pub trait ExternalResolver: Send + Sync {
- /// e.g. `"github.com"`
- fn domain_match(&self) -> &'static str;
-
- /// Normalizes URLs (e.g. stripping fragments); extend per-domain later.
- fn normalize(&self, path: &str) -> String;
-
- /// Fetches body when missing; GitHub hook lands here in a follow-up.
- async fn fetch_body(&self, item: &ItemId) -> Result<String, String>;
-}
-
-#[derive(Clone)]
-pub struct GitHubResolver {
- client: reqwest::Client,
- api_base_url: String,
- token: Option<String>,
-}
-
-impl GitHubResolver {
- pub fn from_env() -> Self {
- let api_base_url = std::env::var("SLUG_GITHUB_API_BASE_URL")
- .ok()
- .filter(|s| !s.trim().is_empty())
- .unwrap_or_else(|| "https://api.github.com".to_string());
- let token = std::env::var("SLUG_GITHUB_TOKEN")
- .ok()
- .filter(|s| !s.trim().is_empty());
- Self {
- client: reqwest::Client::new(),
- api_base_url: api_base_url.trim_end_matches('/').to_string(),
- token,
- }
- }
-
- pub fn can_resolve_children(&self, item: &ItemId) -> bool {
- github_segments(item).is_some()
- }
-
- pub async fn list_children(&self, item: &ItemId) -> Result<Vec<ResolvedChild>, String> {
- let segments = github_segments(item).ok_or_else(|| "not a GitHub URL".to_string())?;
- match segments.as_slice() {
- [] => Ok(vec![]),
- [owner] => self.list_repos(owner).await,
- [owner, repo] => Ok(github_repo_sections(owner, repo)),
- [owner, repo, section] if section == "issues" => self.list_issues(owner, repo).await,
- [owner, repo, section] if section == "pulls" => self.list_pulls(owner, repo).await,
- [owner, repo, section] if section == "commits" => self.list_commits(owner, repo).await,
- [owner, repo, section] if section == "releases" => {
- self.list_releases(owner, repo).await
- }
- _ => Ok(vec![]),
- }
- }
-
- async fn get_json(&self, path: &str) -> Result<Value, String> {
- let url = format!("{}/{}", self.api_base_url, path.trim_start_matches('/'));
- let mut req = self
- .client
- .get(url)
- .header(reqwest::header::USER_AGENT, "slugsocial-github-resolver");
- if let Some(token) = &self.token {
- req = req.bearer_auth(token);
- }
- let resp = req
- .send()
- .await
- .map_err(|e| format!("GitHub request failed: {e}"))?;
- let status = resp.status();
- if !status.is_success() {
- return Err(format!("GitHub request returned {status}"));
- }
- resp.json::<Value>()
- .await
- .map_err(|e| format!("GitHub response JSON failed: {e}"))
- }
-
- async fn get_json_array_pages(&self, path: &str) -> Result<Vec<Value>, String> {
- let sep = if path.contains('?') { '&' } else { '?' };
- let mut out = Vec::new();
- for page in 1..=GITHUB_MAX_PAGES {
- let value = self.get_json(&format!("{path}{sep}page={page}")).await?;
- let arr = value
- .as_array()
- .ok_or_else(|| "GitHub paged response was not an array".to_string())?;
- let n = arr.len();
- out.extend(arr.iter().cloned());
- if n < 100 {
- break;
- }
- }
- Ok(out)
- }
-
- async fn list_repos(&self, owner: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!(
- "/users/{owner}/repos?per_page=100&sort=updated&type=owner"
- ))
- .await?;
- let mut out = Vec::new();
- for repo in &arr {
- let name = repo
- .get("name")
- .and_then(|v| v.as_str())
- .unwrap_or_default();
- if name.is_empty() {
- continue;
- }
- let full_name = repo
- .get("full_name")
- .and_then(|v| v.as_str())
- .map(|s| s.to_ascii_lowercase())
- .unwrap_or_else(|| format!("{owner}/{name}").to_ascii_lowercase());
- out.push(ResolvedChild {
- url: format!("https://github.com/{full_name}"),
- title: full_name.clone(),
- body: Some(github_repo_body(repo)),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_issues(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!(
- "/repos/{owner}/{repo}/issues?state=open&per_page=100"
- ))
- .await?;
- let mut out = Vec::new();
- for issue in &arr {
- if issue.get("pull_request").is_some() {
- continue;
- }
- let Some(number) = issue.get("number").and_then(|v| v.as_i64()) else {
- continue;
- };
- let title = issue
- .get("title")
- .and_then(|v| v.as_str())
- .unwrap_or("Untitled issue");
- out.push(ResolvedChild {
- url: format!("https://github.com/{owner}/{repo}/issues/{number}"),
- title: format!("#{number} {title}"),
- body: Some(github_issue_body(issue, "issue")),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_pulls(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!(
- "/repos/{owner}/{repo}/pulls?state=open&per_page=100"
- ))
- .await?;
- let mut out = Vec::new();
- for pull in &arr {
- let Some(number) = pull.get("number").and_then(|v| v.as_i64()) else {
- continue;
- };
- let title = pull
- .get("title")
- .and_then(|v| v.as_str())
- .unwrap_or("Untitled pull request");
- out.push(ResolvedChild {
- url: format!("https://github.com/{owner}/{repo}/pulls/{number}"),
- title: format!("#{number} {title}"),
- body: Some(github_issue_body(pull, "pull request")),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_commits(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!("/repos/{owner}/{repo}/commits?per_page=100"))
- .await?;
- let mut out = Vec::new();
- for commit in &arr {
- let Some(sha) = github_string(commit, "sha") else {
- continue;
- };
- let short = sha.chars().take(7).collect::<String>();
- let title = commit
- .get("commit")
- .and_then(|c| c.get("message"))
- .and_then(|v| v.as_str())
- .and_then(|m| m.lines().next())
- .filter(|s| !s.trim().is_empty())
- .unwrap_or("commit");
- let url = github_string(commit, "html_url")
- .map(|s| s.to_string())
- .unwrap_or_else(|| format!("https://github.com/{owner}/{repo}/commit/{sha}"));
- out.push(ResolvedChild {
- url,
- title: format!("{short} {title}"),
- body: Some(github_commit_body(commit)),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_releases(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr =
… preview truncated; 60,917 characters omittedHardlinks — judgments / attempts / prompt
judgments
- ~anthropic/claude-sonnet-latest: B (6:4)
- ~x-ai/grok-latest: A (3:1)
- openai/gpt-chat-latest: A (4:1)
- openai/gpt-5.3-chat: A (3:2)
- openai/gpt-5.2-chat: A (3:2)
attempts
- ~anthropic/claude-sonnet-latest #1
- ~x-ai/grok-latest #1
- openai/gpt-chat-latest #1
- openai/gpt-5.3-chat #1
- openai/gpt-5.2-chat #1
Prompt text is loaded only by the download route.