{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[aa64d2f6] nice\n\nSide A — unified diff (full patch):\ndiff --git a/server/src/api/helpers.rs b/server/src/api/helpers.rs\nindex 8cd23a02fa5219d6aa375e766e6b2bd2c7bb7dfb..6a78c7346e3505b54f2587bbca402379bb929f81 100644\n--- a/server/src/api/helpers.rs\n+++ b/server/src/api/helpers.rs\n@@ -31,13 +31,13 @@ pub fn now_ms() -> i64 {\n t.as_millis() as i64\n }\n \n-/// Resolve DSL/user input to a stored canonical item id.\n+/// Resolve DSL/user input to a stored [`ItemId`].\n pub fn resolve_item(item: &str) -> Result {\n- let canonical = canonicalize_item(item);\n- if canonical.is_empty() {\n+ let wire = canonicalize_item(item);\n+ if wire.is_empty() {\n return Err(format!(\"empty item path: `{}`\", item));\n }\n- ItemId::parse(&canonical).ok_or_else(|| format!(\"invalid item path: `{}`\", item))\n+ ItemId::parse(&wire).ok_or_else(|| format!(\"invalid item path: `{}`\", item))\n }\n \n pub fn parse_parent_specs(parent: Option<&String>) -> Vec {\n@@ -95,7 +95,7 @@ pub fn paginate_rankings(\n (out_components, out_unranked)\n }\n \n-pub fn pick_random_distinct_canonical(items: &[ItemId]) -> Option<(ItemId, ItemId)> {\n+pub fn pick_random_distinct_item_pair(items: &[ItemId]) -> Option<(ItemId, ItemId)> {\n use rand::seq::SliceRandom;\n if items.len() < 2 {\n return None;\ndiff --git a/server/src/api/mod.rs b/server/src/api/mod.rs\nindex 1528b3ff69e8d634158ee56f6b658752a3b90e94..9f3c1cc21c2ae157c024a447980a97e190c8f066 100644\n--- a/server/src/api/mod.rs\n+++ b/server/src/api/mod.rs\n@@ -27,7 +27,7 @@ pub use auth::{\n \n pub use helpers::{\n api_error, compute_connectivity_stats, is_pair_voted, now_ms, paginate_rankings,\n- parse_parent_specs, pick_random_distinct_canonical, resolve_item, sha256_hex, vote_touches_path,\n+ parse_parent_specs, pick_random_distinct_item_pair, resolve_item, sha256_hex, vote_touches_path,\n };\n \n pub use rpc::handle_rpc_batch;\ndiff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs\nindex 079d96eb6f1d717c203b1d4aec09f3916384b919..470e54413b9e98825a084a5095c576784c19e169 100644\n--- a/server/src/api/rpc.rs\n+++ b/server/src/api/rpc.rs\n@@ -27,7 +27,7 @@ use crate::{\n use super::auth::{parse_bearer, verify_bearer_principal};\n use super::helpers::{\n compute_connectivity_stats, is_pair_voted, now_ms, paginate_rankings, parse_parent_specs,\n- pick_random_distinct_canonical, resolve_item, vote_touches_path,\n+ pick_random_distinct_item_pair, resolve_item, vote_touches_path,\n };\n use super::validate::validate_ingest_document;\n \n@@ -721,7 +721,7 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n let content = reduced.content.entry(scope.clone()).or_default();\n let group = &mut content.ranking_group;\n if group.idx_to_item.is_empty() {\n- pick_random_distinct_canonical(&pool)\n+ pick_random_distinct_item_pair(&pool)\n } else {\n let mut rng = rand::thread_rng();\n let idxs: Vec = pool\n@@ -767,7 +767,7 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n }\n }\n }\n- pick.or_else(|| pick_random_distinct_canonical(&pool))\n+ pick.or_else(|| pick_random_distinct_item_pair(&pool))\n }\n };\n let Some((left, right)) = selected else {\ndiff --git a/server/src/external_resolver.rs b/server/src/external_resolver.rs\nindex 6f5c982a627f9f620f0e3be0ba0cb92a3d7d4bb7..13f06b922a685f110d269f7397da807cf8600fe1 100644\n--- a/server/src/external_resolver.rs\n+++ b/server/src/external_resolver.rs\n@@ -11,7 +11,7 @@ pub trait ExternalResolver: Send + Sync {\n fn normalize(&self, path: &str) -> String;\n \n /// Fetches body when missing; GitHub hook lands here in a follow-up.\n- async fn fetch_body(&self, canonical_url: &ItemId) -> Result;\n+ async fn fetch_body(&self, item: &ItemId) -> Result;\n }\n \n /// Placeholder until domain-specific resolvers exist.\n@@ -27,7 +27,7 @@ impl ExternalResolver for DefaultExternalResolver {\n path.to_string()\n }\n \n- async fn fetch_body(&self, _canonical_url: &ItemId) -> Result {\n+ async fn fetch_body(&self, _item: &ItemId) -> Result {\n Err(\"external fetch not implemented\".to_string())\n }\n }\ndiff --git a/server/src/html/breadcrumb_path.rs b/server/src/html/breadcrumb_path.rs\nindex 5743a98753f579c70e10961278469afd7cb9ddcf..3e949ce2cb70de3f024f46af773a93f3f8852260 100644\n--- a/server/src/html/breadcrumb_path.rs\n+++ b/server/src/html/breadcrumb_path.rs\n@@ -2,33 +2,33 @@ use crate::path_types::{tilde_http_path_to_item_id, ItemId};\n \n /// Semantic view of an ontology path for rendering and routing decisions.\n pub(super) struct OntologyPath {\n- canonical: ItemId,\n+ item: ItemId,\n /// Breadcrumb segments: for `~/a/b` this is `[\"a\", \"b\"]` (leading `~` rendered separately).\n segments: Vec,\n }\n \n impl OntologyPath {\n /// Path is the `*path` segment from `/~/*path` (e.g. `topic/a`). Always treat it as under `~/`\n- /// so it canonicalizes to `https://slug.social/~/…`, not the non-tilde site path.\n+ /// so it resolves to `https://slug.social/~/…`, not the non-tilde site path.\n pub(super) fn from_input(path: &str) -> Self {\n- let canonical = tilde_http_path_to_item_id(path);\n- Self::from_canonical(canonical)\n+ let item = tilde_http_path_to_item_id(path);\n+ Self::from_item(item)\n }\n \n- pub(super) fn from_canonical(canonical: ItemId) -> Self {\n+ pub(super) fn from_item(item: ItemId) -> Self {\n // tilde_segments() returns [\"~\", \"a\", \"b\"] but bc_path() renders \"~\" itself,\n // so we skip the leading \"~\" segment here.\n- let segments = canonical\n+ let segments = item\n .tilde_segments()\n .into_iter()\n .skip(1) // drop the leading \"~\"\n .map(|s| s.to_string())\n .collect();\n- Self { canonical, segments }\n+ Self { item, segments }\n }\n \n pub(super) fn root() -> Self {\n- Self::from_canonical(ItemId::ontology_root())\n+ Self::from_item(ItemId::ontology_root())\n }\n \n pub(super) fn is_root(&self) -> bool {\n@@ -50,13 +50,13 @@ impl OntologyPath {\n }\n \n pub(super) fn as_str(&self) -> &str {\n- self.canonical.as_str()\n+ self.item.as_str()\n }\n }\n \n /// External `https://host/…` items addressed as `/-/host/…` in the URL bar.\n pub(super) struct ExternalOntologyPath {\n- canonical: ItemId,\n+ item: ItemId,\n /// e.g. `[\"github.com\", \"org\", \"repo\", \"issues\"]`\n segments: Vec,\n }\n@@ -71,14 +71,14 @@ impl ExternalOntologyPath {\n } else {\n format!(\"-/{}\", p.trim_start_matches('/'))\n };\n- let Some(canonical) = ItemId::parse(&raw) else {\n- return Self::from_canonical(ItemId::opaque(\"https://.\".to_string()));\n+ let Some(parsed) = ItemId::parse(&raw) else {\n+ return Self::from_item(ItemId::opaque(\"https://.\".to_string()));\n };\n- Self::from_canonical(canonical)\n+ Self::from_item(parsed)\n }\n \n- pub(super) fn from_canonical(canonical: ItemId) -> Self {\n- let s = canonical.as_str();\n+ pub(super) fn from_item(item: ItemId) -> Self {\n+ let s = item.as_str();\n let rest = s\n .strip_prefix(\"https://\")\n .or_else(|| s.strip_prefix(\"http://\"))\n@@ -93,7 +93,7 @@ impl ExternalOntologyPath {\n } else {\n segments\n };\n- Self { canonical, segments }\n+ Self { item, segments }\n }\n \n pub(super) fn is_root(&self) -> bool {\n@@ -105,6 +105,6 @@ impl ExternalOntologyPath {\n }\n \n pub(super) fn as_str(&self) -> &str {\n- self.canonical.as_str()\n+ self.item.as_str()\n }\n }\ndiff --git a/server/src/html/garden.rs b/server/src/html/garden.rs\nindex 2af9ac5bfdee1630883e6f8257883baeafcb5a44..01b665b207f7ad4a2dac60153b9165679aaa9ab7 100644\n--- a/server/src/html/garden.rs\n+++ b/server/src/html/garden.rs\n@@ -32,7 +32,7 @@ fn item_display_path(item: &str) -> String {\n .unwrap_or_else(|| canonicalize_item(item))\n }\n \n-/// Canonical ontology URL for an item path.\n+/// Garden href for an item path string in this nav scope.\n fn item_href(item: &str, nav: &ThreadNav) -> String {\n nav.garden_item_url(item)\n }\n@@ -67,7 +67,7 @@ enum GardenBrowsePath {\n }\n \n impl GardenBrowsePath {\n- fn canonical_url(&self) -> &str {\n+ fn item(&self) -> &str {\n match self {\n GardenBrowsePath::Tilde(p) => p.as_str(),\n GardenBrowsePath::External(p) => p.as_str(),\n@@ -669,7 +669,7 @@ async fn render_scope_view(\n let scope = nav.scope();\n let model = {\n let reduced = state.reduced.read().await;\n- build_item_page_view_model(&reduced, &scope, browse.canonical_url())\n+ build_item_page_view_model(&reduced, &scope, browse.item())\n };\n let thread_href = |tag: &str| nav.thread_url(tag);\n let external_empty_body = browse.is_external() && model.body.is_none();\ndiff --git a/server/src/html/mod.rs b/server/src/html/mod.rs\nindex 668403a35c415e6f091362b28c63ac294057fb6d..d6571df9b98726c0d3a82516e13404b74966281e 100644\n--- a/server/src/html/mod.rs\n+++ b/server/src/html/mod.rs\n@@ -376,7 +376,7 @@ fn bc_path_external(path: &ExternalOntologyPath) -> Markup {\n }\n }\n \n-/// Breadcrumb for any canonical path, e.g. \"parables\" or \"parables/counting-the-cost\".\n+/// Breadcrumb for any ontology path segment, e.g. \"parables\" or \"parables/counting-the-cost\".\n fn bc_path(path: &OntologyPath) -> Markup {\n html! {\n a href=(path.slug_root_href()) { \"slug.social\" }\ndiff --git a/server/src/reducer.rs b/server/src/reducer.rs\nindex cd11a5391de8e36b1c3bb6c9b8067ceba0878e31..8751595b1dbe472079021994bd452a0234d59af5 100644\n--- a/server/src/reducer.rs\n+++ b/server/src/reducer.rs\n@@ -88,7 +88,7 @@ impl GroupState {\n if let Some(canon) = ItemId::parse(item) {\n self.ensure_item(&canon)\n } else {\n- // Fallback: treat as raw canonical string\n+ // Fallback: treat as raw storage string\n let canon = ItemId::opaque(item.to_string());\n self.ensure_item(&canon)\n }\n@@ -212,7 +212,7 @@ pub struct ContentState {\n pub ranking_group: GroupState,\n pub items: HashSet,\n pub item_bodies: HashMap,\n- /// Parent canonical URL -> direct children.\n+ /// Parent [`ItemId`] -> direct children.\n pub item_children: HashMap>,\n /// Per-item vote history (most recent first).\n pub item_votes: HashMap>,\n@@ -365,7 +365,7 @@ impl ReducerState {\n }\n }\n \n- /// Resolve an item path as a first-class canonical path.\n+ /// Resolve an item path as a first-class [`ItemId`].\n fn normalize_item(item: &str) -> Option {\n ItemId::parse(item)\n }\ndiff --git a/types/src/item_id.rs b/types/src/item_id.rs\nindex 5fe9fad39c0d58e546c0a0162eba4348d64e0865..3411c35cd642d4bc4fc27fcb96a238d32dbd07a5 100644\n--- a/types/src/item_id.rs\n+++ b/types/src/item_id.rs\n@@ -12,7 +12,7 @@ use crate::item_wire::{\n \n /// Structural key for items in [`slug_types`] and the server reducer.\n ///\n-/// Wire / JSON uses the same single string as the former canonical item URL (via serde).\n+/// Wire / JSON uses the same single normalized storage string (via serde).\n #[derive(Debug, Clone, Hash, PartialEq, Eq)]\n pub enum ItemId {\n /// Tilde ontology root (`~/`); storage [`SLUG_TILDE_ONTOLOGY_ROOT`].\n@@ -65,6 +65,8 @@ impl ItemId {\n self.as_str().to_string()\n }\n \n+ /// Tail after `https://slug.social/~/` for ontology items (empty string at root), or `None`\n+ /// when this id is not under the tilde ontology.\n pub fn tilde_tail(&self) -> Option<&str> {\n match self {\n ItemId::Root => Some(\"\"),\n@@ -81,11 +83,6 @@ impl ItemId {\n }\n }\n \n- /// HTTP garden tail after `~/` (empty at ontology root), or `None` if not under tilde ontology.\n- pub fn tilde_http_tail(&self) -> Option {\n- self.tilde_tail().map(str::to_owned)\n- }\n-\n pub fn last_segment(&self) -> &str {\n let s = self.as_str();\n s.rsplit('/').find(|x| !x.is_empty()).unwrap_or(s)\ndiff --git a/types/src/paths.rs b/types/src/paths.rs\nindex a1d066d83e7fbcefc27707dd682e54dbcec1cc56..8669f6872b9fa71d72a3f5c346945427bc00ac5a 100644\n--- a/types/src/paths.rs\n+++ b/types/src/paths.rs\n@@ -334,19 +334,19 @@ mod tests {\n use super::*;\n \n #[test]\n- fn canonical_parent_deep() {\n+ fn item_parent_deep() {\n let c = ItemId::parse(\"~/a/b/c\").unwrap();\n assert_eq!(c.parent().unwrap().as_str(), \"https://slug.social/~/a/b\");\n }\n \n #[test]\n- fn canonical_parent_one_level() {\n+ fn item_parent_one_level() {\n let c = ItemId::parse(\"~/a\").unwrap();\n assert_eq!(c.parent().unwrap().as_str(), \"https://slug.social/~\");\n }\n \n #[test]\n- fn canonical_parent_root_is_none() {\n+ fn item_parent_root_is_none() {\n let root = ItemId::parse(\"~/\").unwrap();\n assert!(root.parent().is_none());\n assert_eq!(root.as_str(), SLUG_TILDE_ONTOLOGY_ROOT);\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[af27c3ab] oicd first pass for npm\n\nSide B — unified diff (full patch):\ndiff --git a/.github/workflows/release.yml b/.github/workflows/release.yml\nindex faed75894dace9edcde19eb10753234a1c1261e9..6f3e27e491b56aaaa1ef64c547cc68921e010b3f 100644\n--- a/.github/workflows/release.yml\n+++ b/.github/workflows/release.yml\n@@ -88,9 +88,12 @@ jobs:\n \n - uses: actions/setup-node@v4\n with:\n- node-version: 20\n+ node-version: 22\n registry-url: \"https://registry.npmjs.org\"\n \n+ - name: Upgrade npm for OIDC trusted publishing support\n+ run: npm install -g npm@latest\n+\n - name: Copy binaries into npm platform packages\n shell: bash\n run: |\n@@ -125,8 +128,6 @@ jobs:\n \n - name: Publish npm platform packages\n shell: bash\n- env:\n- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n run: |\n set -euxo pipefail\n for pkg in packages/npm/platforms/*; do\n@@ -135,15 +136,10 @@ jobs:\n \n - name: Publish npm root package\n shell: bash\n- env:\n- # Unscoped packages need a token that is allowed to create/publish unscoped names.\n- # GitHub's setup-node expects NODE_AUTH_TOKEN. See:\n- # https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages\n- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_UNSCOPED }}\n run: |\n set -euxo pipefail\n cd packages/npm\n- npm publish --access public --provenance\n+ npm publish --access public\n \n publish-pypi:\n name: publish PyPI\n","role":"user"}],"model":"openai/gpt-chat-latest"}