{"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[674964ef] refactor: Deref for href newtypes, CanonicalItemUrl through resolve_item\n\n- Implement Deref for GardenItemUrl, ForumThreadUrl, TildeOntologyPath\n- resolve_item returns CanonicalItemUrl; validate uses HashSet\n- compute_scope_rank_changes keys are CanonicalItemUrl; pair RPC uses Vec pool\n- pick_random_distinct_canonical; connectivity stats on &[CanonicalItemUrl]\n- Global rank unranked uses stored ids before GardenItemUrl mapping\n\nMade-with: Cursor\n\nSide A — unified diff (full patch):\ndiff --git a/server/src/api/helpers.rs b/server/src/api/helpers.rs\nindex 03b3e77911ccd662bec8635345dafe2593cf242e..1b291db83df7364a026f2e147e0a29a70a399371 100644\n--- a/server/src/api/helpers.rs\n+++ b/server/src/api/helpers.rs\n@@ -30,13 +30,13 @@ pub fn now_ms() -> i64 {\n t.as_millis() as i64\n }\n \n-/// Resolve an item path as a first-class canonical path.\n-pub fn resolve_item(item: &str) -> Result {\n+/// Resolve DSL/user input to a stored canonical item id.\n+pub fn resolve_item(item: &str) -> Result {\n let canonical = canonicalize_item(item);\n if canonical.is_empty() {\n return Err(format!(\"empty item path: `{}`\", item));\n }\n- Ok(canonical)\n+ Ok(CanonicalItemUrl(canonical))\n }\n \n pub fn parse_parent_specs(parent: Option<&String>) -> Vec {\n@@ -94,7 +94,7 @@ pub fn paginate_rankings(\n (out_components, out_unranked)\n }\n \n-pub fn pick_random_distinct(items: &[String]) -> Option<(String, String)> {\n+pub fn pick_random_distinct_canonical(items: &[CanonicalItemUrl]) -> Option<(CanonicalItemUrl, CanonicalItemUrl)> {\n use rand::seq::SliceRandom;\n if items.len() < 2 {\n return None;\n@@ -123,15 +123,12 @@ pub fn is_pair_voted(group: &crate::reducer::GroupState, a: &str, b: &str) -> bo\n group.voted_pairs.contains(&(i, j))\n }\n \n-pub fn compute_connectivity_stats(group: &crate::reducer::GroupState, pool: &[String]) -> ConnectivityStats {\n+pub fn compute_connectivity_stats(group: &crate::reducer::GroupState, pool: &[CanonicalItemUrl]) -> ConnectivityStats {\n let n = pool.len();\n \n let global_idxs: Vec> = pool\n .iter()\n- .map(|it| {\n- let key = CanonicalItemUrl(it.clone());\n- group.item_to_idx.get(&key).copied()\n- })\n+ .map(|it| group.item_to_idx.get(it).copied())\n .collect();\n let present: Vec = global_idxs.iter().filter_map(|x| *x).collect();\n \ndiff --git a/server/src/api/mod.rs b/server/src/api/mod.rs\nindex cf22cb0129366c3aed031bc86f3197a4321cb806..a10ce662105cff8fad949c6b83f7035ce79bed18 100644\n--- a/server/src/api/mod.rs\n+++ b/server/src/api/mod.rs\n@@ -24,7 +24,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, resolve_item, sha256_hex, vote_touches_path,\n+ parse_parent_specs, pick_random_distinct_canonical, 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 5f7d50188f1381267402f2e57e671234ef5db2fd..de0955d0887d32740e1fd18365205c5bdb53c247 100644\n--- a/server/src/api/rpc.rs\n+++ b/server/src/api/rpc.rs\n@@ -29,7 +29,7 @@ use crate::{\n use super::auth::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, resolve_item, vote_touches_path,\n+ pick_random_distinct_canonical, resolve_item, vote_touches_path,\n };\n use super::validate::{normalize_room_and_thread, validate_ingest_document};\n \n@@ -146,21 +146,21 @@ fn authorize_room_read(reduced: &ReducerState, headers: &HeaderMap, room: &str)\n }\n \n fn compute_scope_rank_changes(\n- parent: &str,\n+ parent: &CanonicalItemUrl,\n before: &crate::scope_rank::ChildrenRankings,\n after: &crate::scope_rank::ChildrenRankings,\n room_wire: &str,\n ) -> Option {\n- fn build_positions(rankings: &crate::scope_rank::ChildrenRankings) -> HashMap> {\n+ fn build_positions(rankings: &crate::scope_rank::ChildrenRankings) -> HashMap> {\n let mut map = HashMap::new();\n for comp in &rankings.component_rankings {\n let total = comp.ranked.len();\n for (i, item) in comp.ranked.iter().enumerate() {\n- map.insert(item.item.as_str().to_string(), Some(RankPosition { rank: i + 1, of: total }));\n+ map.insert(item.item.clone(), Some(RankPosition { rank: i + 1, of: total }));\n }\n }\n for item in &rankings.unranked_items {\n- map.insert(item.as_str().to_string(), None);\n+ map.insert(item.clone(), None);\n }\n map\n }\n@@ -168,7 +168,7 @@ fn compute_scope_rank_changes(\n let before_pos = build_positions(before);\n let after_pos = build_positions(after);\n \n- let all_items: std::collections::BTreeSet = before_pos.keys().cloned()\n+ let all_items: std::collections::BTreeSet = before_pos.keys().cloned()\n .chain(after_pos.keys().cloned())\n .collect();\n \n@@ -184,7 +184,7 @@ fn compute_scope_rank_changes(\n };\n if changed {\n changes.push(RankChange {\n- item: GardenItemUrl::from_storage_str(&item, room_wire),\n+ item: GardenItemUrl::from_stored(&item, room_wire),\n before: b,\n after: a,\n });\n@@ -203,11 +203,7 @@ fn compute_scope_rank_changes(\n });\n \n Some(ScopeRankChanges {\n- parent: if parent.is_empty() {\n- \"/\".to_string()\n- } else {\n- GardenItemUrl::from_storage_str(parent, room_wire).into_inner()\n- },\n+ parent: GardenItemUrl::from_stored(parent, room_wire).into_inner(),\n changes,\n })\n }\n@@ -473,8 +469,8 @@ async fn rpc_post(\n for s in &v.doc.statements {\n if let dsl::Stmt::Vote { item1, item2, .. } = s {\n if let (Ok(a), Ok(b)) = (resolve_item(item1), resolve_item(item2)) {\n- if let Some(p) = CanonicalItemUrl::parse(&a).and_then(|c| c.parent()) { parents.insert(p); }\n- if let Some(p) = CanonicalItemUrl::parse(&b).and_then(|c| c.parent()) { parents.insert(p); }\n+ if let Some(p) = a.parent() { parents.insert(p); }\n+ if let Some(p) = b.parent() { parents.insert(p); }\n }\n }\n }\n@@ -525,7 +521,7 @@ async fn rpc_post(\n .filter_map(|p| {\n let before = pre_rankings.get(p)?;\n let after = crate::scope_rank::build_children_rankings(content, p);\n- compute_scope_rank_changes(p.as_str(), before, &after, &room_key)\n+ compute_scope_rank_changes(p, before, &after, &room_key)\n })\n .collect();\n if v.is_empty() { None } else { Some(v) }\n@@ -638,8 +634,8 @@ async fn rpc_check(\n for s in &v.doc.statements {\n if let dsl::Stmt::Vote { item1, item2, .. } = s {\n if let (Ok(a), Ok(b)) = (resolve_item(item1), resolve_item(item2)) {\n- if let Some(p) = CanonicalItemUrl::parse(&a).and_then(|c| c.parent()) { parents.insert(p); }\n- if let Some(p) = CanonicalItemUrl::parse(&b).and_then(|c| c.parent()) { parents.insert(p); }\n+ if let Some(p) = a.parent() { parents.insert(p); }\n+ if let Some(p) = b.parent() { parents.insert(p); }\n }\n }\n }\n@@ -961,7 +957,7 @@ fn rpc_search(reduced: &ReducerState, q: &str, limit: usize, principal: Option<&\n async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Result {\n let scope = scope_from_room_wire(&room);\n let reduced_arc = state.reduced.clone();\n- let pool: Vec = {\n+ let pool: Vec = {\n let reduced = reduced_arc.read().await;\n let content = content_for_room(&reduced, &room);\n let tmp = if parent_path.trim().is_empty() {\n@@ -970,12 +966,11 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n Some(parent_path.clone())\n };\n let specs = parse_parent_specs(tmp.as_ref());\n- let raw_pool: Vec = if specs.is_empty() {\n+ if specs.is_empty() {\n content.ranking_group.idx_to_item.clone()\n } else {\n crate::scope_rank::resolve_scope(content, &specs)\n- };\n- raw_pool.into_iter().map(|it| it.0).collect()\n+ }\n };\n if pool.len() < 2 {\n return Err((\n@@ -983,31 +978,30 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n Some(\"add items via ingest\".into()),\n ));\n }\n- let selected: Option<(String, String)> = {\n+ let selected: Option<(CanonicalItemUrl, CanonicalItemUrl)> = {\n let mut reduced = reduced_arc.write().await;\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(&pool)\n+ pick_random_distinct_canonical(&pool)\n } else {\n let mut rng = rand::thread_rng();\n- let idxs: Vec = pool.iter()\n- .filter_map(|it| {\n- let key = CanonicalItemUrl(it.clone());\n- group.item_to_idx.get(&key).copied()\n- })\n+ let idxs: Vec = pool\n+ .iter()\n+ .filter_map(|it| group.item_to_idx.get(it).copied())\n .collect();\n let ranked = ranked_items_subset(group, &idxs, 10000, 1e-8);\n- let ranked_set: HashSet = ranked.iter().map(|r| r.item.as_str().to_string()).collect();\n- let unsorted: Vec = pool.iter()\n+ let ranked_set: HashSet = ranked.iter().map(|r| r.item.clone()).collect();\n+ let unsorted: Vec = pool\n+ .iter()\n .filter(|it| !ranked_set.contains(*it))\n .cloned()\n .collect();\n- let mut pick: Option<(String, String)> = None;\n+ let mut pick: Option<(CanonicalItemUrl, CanonicalItemUrl)> = None;\n if !unsorted.is_empty() {\n if let Some(left) = unsorted.choose(&mut rng).cloned() {\n- let mut candidates: Vec = if !ranked.is_empty() {\n- ranked.iter().map(|r| r.item.as_str().to_string()).collect()\n+ let mut candidates: Vec = if !ranked.is_empty() {\n+ ranked.iter().map(|r| r.item.clone()).collect()\n } else {\n pool.clone()\n };\n@@ -1021,21 +1015,21 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n let a = ranked[i].item.as_str();\n let b = ranked[i + 1].item.as_str();\n if a != b && !is_pair_voted(group, a, b) {\n- pick = Some((a.to_string(), b.to_string()));\n+ pick = Some((ranked[i].item.clone(), ranked[i + 1].item.clone()));\n break;\n }\n }\n if pick.is_none() {\n for _ in 0..64 {\n let (Some(a), Some(b)) = (pool.choose(&mut rng).cloned(), pool.choose(&mut rng).cloned()) else { break; };\n- if a != b && !is_pair_voted(group, &a, &b) {\n+ if a != b && !is_pair_voted(group, a.as_str(), b.as_str()) {\n pick = Some((a, b));\n break;\n }\n }\n }\n }\n- pick.or_else(|| pick_random_distinct(&pool))\n+ pick.or_else(|| pick_random_distinct_canonical(&pool))\n }\n };\n let Some((left, right)) = selected else {\n@@ -1043,8 +1037,8 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n };\n let reduced = reduced_arc.read().await;\n let content = content_for_room(&reduced, &room);\n- let left_key = CanonicalItemUrl(left.clone());\n- let right_key = CanonicalItemUrl(right.clone());\n+ let left_key = left.clone();\n+ let right_key = right.clone();\n let lb = content.item_bodies.get(&left_key).cloned();\n let rb = content.item_bodies.get(&right_key).cloned();\n let th: Vec = content\n@@ -1058,8 +1052,8 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re\n .collect();\n let cs = compute_connectivity_stats(&content.ranking_group, &pool);\n Ok(RpcResult::Pair(PairResponse {\n- left: GardenItemUrl::from_storage_str(&left, &room),\n- right: GardenItemUrl::from_storage_str(&right, &room),\n+ left: GardenItemUrl::from_stored(&left, &room),\n+ right: GardenItemUrl::from_stored(&right, &room),\n left_body: lb,\n right_body: rb,\n threads: th,\n@@ -1554,7 +1548,7 @@ pub async fn handle_rpc_batch(\n for r in items {\n let pct = want_percent.then(|| ((r.score - bot) / range * 100.0).clamp(0.0, 100.0));\n ranked.push(RankRow {\n- item: GardenItemUrl::from_storage_str(r.item.as_str(), &room),\n+ item: GardenItemUrl::from_stored(&r.item, &room),\n score: r.score,\n percent: pct,\n });\n@@ -1562,11 +1556,11 @@ pub async fn handle_rpc_batch(\n }\n \n let ranked_total = ranked.len();\n- let mut unranked: Vec = content\n+ let mut unranked: Vec = content\n .items\n .iter()\n .filter(|it| !group.item_to_idx.contains_key(*it))\n- .map(|it| it.as_str().to_string())\n+ .cloned()\n .collect();\n unranked.sort();\n let unranked_total = unranked.len();\n@@ -1574,7 +1568,7 @@ pub async fn handle_rpc_batch(\n let page: Vec = ranked\n .into_iter()\n .chain(unranked.into_iter().map(|it| RankRow {\n- item: GardenItemUrl::from_storage_str(&it, &room),\n+ item: GardenItemUrl::from_stored(&it, &room),\n score: 0.0,\n percent: want_percent.then_some(0.0),\n }))\n@@ -1722,7 +1716,7 @@ pub async fn handle_rpc_batch(\n .filter(|p| !parents.contains(p.as_str()))\n .map(|p| GardenItemUrl::from_stored(p, &room))\n .collect();\n- paths.sort_by(|a, b| a.as_str().cmp(b.as_str()));\n+ paths.sort();\n line_ok(RpcResult::Leaves(LeavesResponse { paths }))\n }\n },\ndiff --git a/server/src/api/validate.rs b/server/src/api/validate.rs\nindex a51c783ee9785569b5a44c0b1572471fe00d174b..3c7aa80fd485cf247231fe5c5c7e6fea62f22534 100644\n--- a/server/src/api/validate.rs\n+++ b/server/src/api/validate.rs\n@@ -52,7 +52,7 @@ pub fn validate_ingest_document(\n };\n \n let ts = super::helpers::now_ms();\n- let mut defined_in_doc: HashSet = HashSet::new();\n+ let mut defined_in_doc: HashSet = HashSet::new();\n \n for s in &doc.statements {\n match s {\n@@ -66,14 +66,14 @@ pub fn validate_ingest_document(\n let Some(body_text) = body else {\n return Err((\n StatusCode::BAD_REQUEST,\n- format!(\"item missing body: {}\", GardenItemUrl::from_storage_str(&item, room_wire)),\n+ format!(\"item missing body: {}\", GardenItemUrl::from_stored(&item, room_wire)),\n Some(\"items must be declared with bodies, e.g. `~/path/item { ... }`\".to_string()),\n ));\n };\n if body_text.trim().is_empty() {\n return Err((\n StatusCode::BAD_REQUEST,\n- format!(\"item body is empty: {}\", GardenItemUrl::from_storage_str(&item, room_wire)),\n+ format!(\"item body is empty: {}\", GardenItemUrl::from_stored(&item, room_wire)),\n Some(\"write at least one sentence inside `{ ... }`\".to_string()),\n ));\n }\n@@ -102,11 +102,8 @@ pub fn validate_ingest_document(\n };\n let missing: Vec = [&a, &b]\n .into_iter()\n- .filter(|it| {\n- let key = CanonicalItemUrl((*it).clone());\n- !defined_in_doc.contains(*it) && !item_exists(&key)\n- })\n- .map(|it| GardenItemUrl::from_storage_str(it, room_wire).into_inner())\n+ .filter(|it| !defined_in_doc.contains(*it) && !item_exists(it))\n+ .map(|it| GardenItemUrl::from_stored(it, room_wire).into_inner())\n .collect();\n if !missing.is_empty() {\n return Err((\n@@ -120,11 +117,8 @@ pub fn validate_ingest_document(\n }\n let missing_body: Vec = [&a, &b]\n .into_iter()\n- .filter(|it| {\n- let key = CanonicalItemUrl((*it).clone());\n- !defined_in_doc.contains(*it) && !body_exists(&key)\n- })\n- .map(|it| GardenItemUrl::from_storage_str(it, room_wire).into_inner())\n+ .filter(|it| !defined_in_doc.contains(*it) && !body_exists(it))\n+ .map(|it| GardenItemUrl::from_stored(it, room_wire).into_inner())\n .collect();\n if !missing_body.is_empty() {\n return Err((\ndiff --git a/server/src/html/editor.rs b/server/src/html/editor.rs\nindex ecdd226b1b17d5f11d1b79f99add5759ef918281..26d648d7c8d6e5d53ce5ed7a051b44856fca2fcf 100644\n--- a/server/src/html/editor.rs\n+++ b/server/src/html/editor.rs\n@@ -130,8 +130,8 @@ pub async fn editor_check(\n for s in &v.doc.statements {\n if let crate::dsl::Stmt::Vote { item1, item2, .. } = s {\n if let (Ok(a), Ok(b)) = (resolve_item(item1), resolve_item(item2)) {\n- if let Some(p) = crate::path_types::CanonicalItemUrl::parse(&a).and_then(|c| c.parent()) { parents.insert(p); }\n- if let Some(p) = crate::path_types::CanonicalItemUrl::parse(&b).and_then(|c| c.parent()) { parents.insert(p); }\n+ if let Some(p) = a.parent() { parents.insert(p); }\n+ if let Some(p) = b.parent() { parents.insert(p); }\n }\n }\n }\ndiff --git a/types/src/paths.rs b/types/src/paths.rs\nindex 2950a7502255927583fbacdfd2adb700f0b0c221..839684dfa0ecdd6c2572e2d8132ecee4d5c41bf8 100644\n--- a/types/src/paths.rs\n+++ b/types/src/paths.rs\n@@ -3,6 +3,7 @@\n \n use std::borrow::Borrow;\n use std::fmt;\n+use std::ops::Deref;\n \n use serde::{Deserialize, Serialize};\n \n@@ -319,6 +320,14 @@ impl fmt::Display for GardenItemUrl {\n }\n }\n \n+impl Deref for GardenItemUrl {\n+ type Target = str;\n+\n+ fn deref(&self) -> &Self::Target {\n+ &self.0\n+ }\n+}\n+\n fn garden_href_string(item: &str, room_wire: &str) -> String {\n let room = room_wire.trim();\n if room.is_empty() || room == \"public\" {\n@@ -386,6 +395,14 @@ impl fmt::Display for ForumThreadUrl {\n }\n }\n \n+impl Deref for ForumThreadUrl {\n+ type Target = str;\n+\n+ fn deref(&self) -> &Self::Target {\n+ &self.0\n+ }\n+}\n+\n /// `~/a/b` style path for list UIs (paths index `path` field).\n #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]\n #[serde(transparent)]\n@@ -412,6 +429,14 @@ impl fmt::Display for TildeOntologyPath {\n }\n }\n \n+impl Deref for TildeOntologyPath {\n+ type Target = str;\n+\n+ fn deref(&self) -> &Self::Target {\n+ &self.0\n+ }\n+}\n+\n #[cfg(test)]\n mod tests {\n use super::*;\n@@ -452,6 +477,13 @@ mod tests {\n assert_eq!(c.tilde_segments(), Vec::<&str>::new());\n }\n \n+ #[test]\n+ fn garden_item_url_deref_to_str() {\n+ let g = GardenItemUrl::from_storage_str(\"https://slug.social/~/x\", \"public\");\n+ let s: &str = &*g;\n+ assert_eq!(s, \"https://slug.social/~/x\");\n+ }\n+\n #[test]\n fn garden_public_passthrough_https() {\n let u = \"https://slug.social/~/a/b\";\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[bda5f8aa] Remove legacy evidence projection; Evidence envelopes only.\n\nEpoch and commit pages no longer invent history from bare GitDiscovery rows. Production ledger will be wiped to re-emit under the current schema.\n\nCo-authored-by: Cursor \n\nSide B — unified diff (full patch):\ndiff --git a/constitution.py b/constitution.py\nindex 4dd5b9dfbba231d46289c490f91b1dd5b1018bcf..26ba130e885e0e69fb7874ca5c3f07f42100a150 100644\n--- a/constitution.py\n+++ b/constitution.py\n@@ -210,10 +210,10 @@ class Emission:\n distributions: dict # author -> amount str\n ranking: dict # author -> score str\n models_used: list\n- discovery_snapshot_id: str = \"\" # empty only for pre-discovery ledger history\n- evidence_schema_version: int = 1\n- ranking_run_id: str = \"\"\n- ranking_event_id: str = \"\"\n+ discovery_snapshot_id: str\n+ evidence_schema_version: int\n+ ranking_run_id: str\n+ ranking_event_id: str\n \n \n @event\n@@ -502,26 +502,6 @@ def _epochs_in_ledger() -> list[int]:\n return sorted(epochs)\n \n \n-def _legacy_commit_row(commit_id: str) -> tuple[GitDiscovery | None, dict | None]:\n- for discovery in store.read():\n- if not isinstance(discovery, GitDiscovery):\n- continue\n- for commit in discovery.commits:\n- if commit_id_for_oid(commit[\"oid\"]) == commit_id:\n- return discovery, commit\n- return None, None\n-\n-\n-def _legacy_observation(commit_id: str) -> tuple[GitDiscovery | None, dict | None]:\n- for discovery in store.read():\n- if not isinstance(discovery, GitDiscovery):\n- continue\n- for obs in discovery.observations:\n- if commit_id_for_oid(obs[\"oid\"]) == commit_id:\n- return discovery, obs\n- return None, None\n-\n-\n def build_pairwise_prompt(side_a: dict, side_b: dict) -> str:\n return f\"\"\"You are ranking contributions to an open source project.\n Compare these two sides (each may be one or more commits). Decide which side contributed more.\n@@ -2040,7 +2020,7 @@ def _strip_heavy_fields(obj: dict) -> dict:\n \n @app.get(\"/api/ledger\")\n async def get_ledger(offset: int = 0, limit: int = 100, full: int = 0):\n- \"\"\"List of ledger dicts (backward-compatible). Heavy blobs stripped unless full=1.\"\"\"\n+ \"\"\"List of ledger dicts. Heavy blobs stripped unless full=1.\"\"\"\n limit = max(1, min(limit, 500))\n rows = []\n for e in store.read()[offset:offset + limit]:\n@@ -2274,23 +2254,25 @@ async def epochs_index():\n epochs = _epochs_in_ledger()\n rows = []\n for epoch in epochs:\n- discovery = _discovery_for_epoch(epoch)\n emission = _emission_for_epoch(epoch)\n- evidence_n = sum(\n- 1 for e in evidence_by_kind() if e.epoch == epoch\n+ evidence_n = sum(1 for e in evidence_by_kind() if e.epoch == epoch)\n+ disc = next(\n+ (\n+ e for e in evidence_by_kind(\"git.discovery_completed\")\n+ if e.epoch == epoch\n+ ),\n+ None,\n )\n detail = []\n- if discovery:\n+ if disc:\n detail.append(\n- f\"{len(discovery.commits)} eligible / \"\n- f\"{len(discovery.observations)} observed\"\n+ f\"{disc.payload.get('eligible_count', 0)} eligible / \"\n+ f\"{disc.payload.get('observation_count', 0)} observed\"\n )\n if emission:\n detail.append(f\"emitted {emission.total_emitted}\")\n if evidence_n:\n detail.append(f\"{evidence_n} evidence events\")\n- elif discovery or emission:\n- detail.append(\"legacy (no Evidence envelopes)\")\n rows.append([\"li\",\n _a(_evidence_path(\"epoch\", str(epoch)), f\"epoch {epoch}\"),\n \" — \",\n@@ -2307,37 +2289,37 @@ async def epochs_index():\n \n @app.get(\"/epochs/{epoch}\")\n async def epoch_detail(epoch: int):\n- discovery = _discovery_for_epoch(epoch)\n emission = _emission_for_epoch(epoch)\n evidence_rows = [e for e in evidence_by_kind() if e.epoch == epoch]\n+ if not evidence_rows and emission is None:\n+ return _evidence_page(f\"epoch {epoch}\", [\n+ _evidence_nav(),\n+ [\"h1\", f\"epoch {epoch}\"],\n+ [\"p.note\", \"No evidence for this epoch.\"],\n+ ])\n+\n commit_evs = [e for e in evidence_rows if e.kind == \"git.commit\"]\n comparison_evs = [e for e in evidence_rows if e.kind == \"comparison.input\"]\n judgment_evs = [e for e in evidence_rows if e.kind == \"llm.judgment\"]\n+ discovery_ev = next(\n+ (e for e in evidence_rows if e.kind == \"git.discovery_completed\"), None\n+ )\n ranking_started = next(\n (e for e in evidence_rows if e.kind == \"ranking.started\"), None\n )\n ranking_completed = next(\n (e for e in evidence_rows if e.kind == \"ranking.completed\"), None\n )\n- legacy = not evidence_rows and (discovery is not None or emission is not None)\n-\n- commit_links: list[tuple[str, str]] = []\n- if commit_evs:\n- for e in commit_evs:\n- cid = e.payload.get(\"commit_id\") or \"\"\n- label = (\n- f\"{e.payload.get('oid', cid)[:24]} \"\n- f\"({e.payload.get('contributor', '?')})\"\n- )\n- commit_links.append((label, _evidence_path(\"commit\", cid)))\n- elif discovery:\n- for c in discovery.commits:\n- cid = commit_id_for_oid(c[\"oid\"])\n- commit_links.append((\n- f\"{c['oid'][:24]} ({c.get('contributor', '?')})\",\n- _evidence_path(\"commit\", cid),\n- ))\n \n+ commit_links = [\n+ (\n+ f\"{e.payload.get('oid', e.payload.get('commit_id', ''))[:24]} \"\n+ f\"({e.payload.get('contributor', '?')})\",\n+ _evidence_path(\"commit\", e.payload[\"commit_id\"]),\n+ )\n+ for e in commit_evs\n+ if e.payload.get(\"commit_id\")\n+ ]\n comparison_links = [\n (\n e.payload.get(\"summary\") or e.payload.get(\"comparison_id\", e.event_id),\n@@ -2360,16 +2342,13 @@ async def epoch_detail(epoch: int):\n ]\n \n excluded = []\n- if discovery:\n- for obs in discovery.observations:\n+ if discovery_ev:\n+ for obs in discovery_ev.payload.get(\"observations\") or []:\n if obs.get(\"eligible\"):\n continue\n oid = obs.get(\"oid\", \"?\")\n reason = obs.get(\"exclusion_reason\") or \"excluded\"\n- excluded.append([\"li\",\n- f\"{oid[:28]} — {reason} — \",\n- [\"span.note\", \"legacy evidence unavailable\"],\n- ])\n+ excluded.append([\"li\", f\"{oid[:28]} — {reason}\"])\n \n ranking_nodes: list = []\n if ranking_completed:\n@@ -2388,21 +2367,10 @@ async def epoch_detail(epoch: int):\n indent=2, sort_keys=True,\n )],\n ]\n- elif emission:\n- if legacy and len(emission.ranking or {}) <= 1:\n- ranking_nodes.append([\"p.note\",\n- \"Single-contributor epoch — no LLM judgments.\"\n- ])\n- ranking_nodes.extend([\n- [\"p\", \"Projected from Emission (no ranking Evidence event).\"],\n- [\"pre.blob\", json.dumps(emission.ranking, indent=2, sort_keys=True)],\n- ])\n+ elif ranking_started:\n+ ranking_nodes = [[\"p.note\", f\"Ranking started: {ranking_started.event_id}\"]]\n else:\n- ranking_nodes = [[\"p.note\", \"No ranking recorded.\"]]\n- if ranking_started and not ranking_completed:\n- ranking_nodes.insert(0, [\"p.note\",\n- f\"Ranking started: {ranking_started.event_id}\"\n- ])\n+ ranking_nodes = [[\"p.note\", \"No ranking evidence.\"]]\n \n if emission:\n emission_node = _dl_rows([\n@@ -2411,44 +2379,41 @@ async def epoch_detail(epoch: int):\n (\"pool_after\", emission.pool_after),\n (\"discovery_snapshot_id\", emission.discovery_snapshot_id),\n (\"ranking_run_id\", emission.ranking_run_id or None),\n+ (\"ranking_event_id\", emission.ranking_event_id or None),\n (\"models_used\", \", \".join(emission.models_used or [])),\n (\"distributions\", json.dumps(emission.distributions, sort_keys=True)),\n ])\n else:\n emission_node = [\"p.note\", \"No emission for this epoch.\"]\n \n- single_contributor = False\n- if discovery:\n- single_contributor = len({c.get(\"contributor\") for c in discovery.commits}) <= 1\n- elif emission:\n- single_contributor = len(emission.ranking or {}) <= 1\n+ contributors = {\n+ e.payload.get(\"contributor\")\n+ for e in commit_evs\n+ if e.payload.get(\"contributor\")\n+ }\n+ no_comparisons_note = \"No comparisons.\"\n+ if len(contributors) <= 1:\n+ no_comparisons_note += \" Single-contributor — no LLM judgments.\"\n \n body = [\n _evidence_nav(),\n [\"div.eyebrow\", f\"epoch {epoch}\"],\n [\"h1\", f\"epoch {epoch}\"],\n ]\n- if legacy:\n- body.append([\"p.note\",\n- \"Legacy epoch: projected from GitDiscovery/Emission without Evidence \"\n- \"envelopes. Eligible commits use discovery patches; discarded observation \"\n- \"metadata is marked legacy evidence unavailable. Single-contributor \"\n- \"epochs have no LLM judgments.\"\n- ])\n- if discovery:\n+ if discovery_ev:\n body.extend([\n [\"h2\", \"discovery\"],\n _dl_rows([\n- (\"snapshot_id\", discovery.snapshot_id),\n- (\"config_digest\", discovery.config_digest),\n- (\"initial_snapshot\", discovery.initial_snapshot),\n- (\"observations\", len(discovery.observations)),\n- (\"eligible\", len(discovery.commits)),\n+ (\"snapshot_id\", discovery_ev.payload.get(\"snapshot_id\")),\n+ (\"config_digest\", discovery_ev.payload.get(\"config_digest\")),\n+ (\"observations\", discovery_ev.payload.get(\"observation_count\")),\n+ (\"eligible\", discovery_ev.payload.get(\"eligible_count\")),\n+ (\"event\", _a(\n+ _evidence_path(\"event\", discovery_ev.event_id),\n+ discovery_ev.event_id,\n+ )),\n ]),\n ])\n- no_comparisons_note = \"No comparisons.\"\n- if single_contributor:\n- no_comparisons_note += \" Single-contributor — no LLM judgments.\"\n body.extend([\n [\"h2\", \"commits\"],\n _link_list(commit_links),\n@@ -2471,92 +2436,42 @@ async def epoch_detail(epoch: int):\n @app.get(\"/commits/{commit_id}\")\n async def commit_detail(commit_id: str):\n ev = find_evidence_payload(\"git.commit\", \"commit_id\", commit_id)\n- discovery, legacy_row = (None, None)\n if not ev:\n- discovery, legacy_row = _legacy_commit_row(commit_id)\n- if not ev and not legacy_row:\n- discovery, obs = _legacy_observation(commit_id)\n- if obs is not None:\n- epoch = discovery.epoch if discovery else \"?\"\n- return _evidence_page(f\"commit {commit_id[:24]}\", [\n- _evidence_nav(\n- _a(_evidence_path(\"epoch\", str(epoch)), f\"epoch {epoch}\")\n- ),\n- [\"div.eyebrow\", \"commit\"],\n- [\"h1\", commit_id],\n- [\"p.note\", \"legacy evidence unavailable\"],\n- _dl_rows([\n- (\"oid\", obs.get(\"oid\")),\n- (\"eligible\", obs.get(\"eligible\")),\n- (\"exclusion_reason\", obs.get(\"exclusion_reason\")),\n- (\"epoch\", str(epoch)),\n- ]),\n- ])\n return _evidence_page(\"commit not found\", [\n _evidence_nav(),\n [\"h1\", \"commit not found\"],\n [\"p\", commit_id],\n ])\n- if ev:\n- p = ev.payload\n- epoch = ev.epoch\n- oid = p.get(\"oid\", \"\")\n- contributor = p.get(\"contributor\", \"\")\n- message = _blob_text(p.get(\"message\"))\n- patch = _blob_text(p.get(\"patch\"))\n- meta = _dl_rows([\n+ p = ev.payload\n+ epoch = ev.epoch\n+ return _evidence_page(f\"commit {commit_id[:24]}\", [\n+ _evidence_nav(_a(_evidence_path(\"epoch\", str(epoch)), f\"epoch {epoch}\")),\n+ [\"div.eyebrow\", \"commit\"],\n+ [\"h1\", commit_id],\n+ _dl_rows([\n (\"commit_id\", commit_id),\n- (\"oid\", oid),\n- (\"contributor\", contributor),\n+ (\"oid\", p.get(\"oid\", \"\")),\n+ (\"contributor\", p.get(\"contributor\", \"\")),\n (\"patch_sha256\", p.get(\"patch_sha256\")),\n (\"patch_identity\", p.get(\"patch_identity\")),\n (\"committer_timestamp_ms\", p.get(\"committer_timestamp_ms\")),\n (\"evidence_event\", _a(_evidence_path(\"event\", ev.event_id), ev.event_id)),\n (\"epoch\", _a(_evidence_path(\"epoch\", str(epoch)), str(epoch))),\n- ])\n- legacy_note = None\n- else:\n- assert legacy_row is not None and discovery is not None\n- epoch = discovery.epoch\n- oid = legacy_row.get(\"oid\", \"\")\n- contributor = legacy_row.get(\"contributor\", \"\")\n- message = legacy_row.get(\"message\") or \"\"\n- patch = legacy_row.get(\"patch\") or \"\"\n- meta = _dl_rows([\n- (\"commit_id\", commit_id),\n- (\"oid\", oid),\n- (\"contributor\", contributor),\n- (\"patch_sha256\", legacy_row.get(\"patch_sha256\")),\n- (\"patch_identity\", legacy_row.get(\"patch_identity\")),\n- (\"committer_timestamp_ms\", legacy_row.get(\"committer_timestamp_ms\")),\n- (\"epoch\", _a(_evidence_path(\"epoch\", str(epoch)), str(epoch))),\n- (\"source\", \"legacy GitDiscovery projection\"),\n- ])\n- legacy_note = [\"p.note\", \"Projected from GitDiscovery — Evidence envelope absent.\"]\n- return _evidence_page(f\"commit {commit_id[:24]}\", [\n- _evidence_nav(_a(_evidence_path(\"epoch\", str(epoch)), f\"epoch {epoch}\")),\n- [\"div.eyebrow\", \"commit\"],\n- [\"h1\", commit_id],\n- *([legacy_note] if legacy_note else []),\n- meta,\n+ ]),\n [\"p\", _a(f\"/commits/{commit_id}/patch\", \"download patch\")],\n [\"h2\", \"message\"],\n- _pre_blob(message),\n+ _pre_blob(_blob_text(p.get(\"message\"))),\n [\"h2\", \"patch\"],\n- _pre_blob(patch),\n+ _pre_blob(_blob_text(p.get(\"patch\"))),\n ])\n \n \n @app.get(\"/commits/{commit_id}/patch\")\n async def commit_patch_download(commit_id: str):\n ev = find_evidence_payload(\"git.commit\", \"commit_id\", commit_id)\n- if ev:\n- raw = _decode_blob(ev.payload.get(\"patch\")) or _blob_text(ev.payload.get(\"patch\")).encode()\n- else:\n- _, row = _legacy_commit_row(commit_id)\n- if not row:\n- return PlainTextResponse(\"not found\", status_code=404)\n- raw = (row.get(\"patch\") or \"\").encode(\"utf-8\")\n+ if not ev:\n+ return PlainTextResponse(\"not found\", status_code=404)\n+ raw = _decode_blob(ev.payload.get(\"patch\")) or _blob_text(ev.payload.get(\"patch\")).encode()\n return Response(\n content=raw,\n media_type=\"text/plain; charset=utf-8\",\n@@ -3226,7 +3141,9 @@ async def watch():\n [\"div.controls\",\n [\"button#play\", {\"type\": \"button\", \"disabled\": \"true\"}, \"▶ Play live feed\"],\n [\"button#pause\", {\"type\": \"button\"}, \"Ⅱ Pause feed\"],\n- [\"span.note\", \"Pause stops local updates; the constitutional process continues.\"],\n+ [\"span.note\",\n+ \"Play/pause only affect this browser feed — they do not run or stop emission. \"\n+ \"The server emits at each epoch boundary; status above is that process.\"],\n ],\n ],\n [\"section.panel\",\ndiff --git a/tests/integration.clj b/tests/integration.clj\nindex 204b0fedfbad57846205cb140c4ed9406b5f8e0e..11140e95a8cd86768a661a30e6b29a3ddfc95fcc 100644\n--- a/tests/integration.clj\n+++ b/tests/integration.clj\n@@ -247,7 +247,10 @@\n :decay_rate \"0.003253356063468\"\n :distributions {\"alice\" \"381.4282558872\" \"bob\" \"190.7141279436\"}\n :ranking {\"alice\" \"0.6666\" \"bob\" \"0.3334\"}\n- :models_used [\"mock/chat-v1\" \"mock/chat-v2\"]}]\n+ :models_used [\"mock/chat-v1\" \"mock/chat-v2\"]\n+ :evidence_schema_version 2\n+ :ranking_run_id \"seed-rank\"\n+ :ranking_event_id \"seed-rank-ev\"}]\n (spit path (str (json/generate-string entry) \"\\n\"))))\n \n ;; ---------------------------------------------------------------------------\ndiff --git a/tests/test_evidence.py b/tests/test_evidence.py\nindex 76adcd343eec3a65dec58fda93b068356fb1adcd..7cb74fd7fb1bcd0ab17d2cbe2e6ca7e61e164221 100644\n--- a/tests/test_evidence.py\n+++ b/tests/test_evidence.py\n@@ -1,4 +1,4 @@\n-\"\"\"HTML evidence graph: byte fidelity, resume, legacy projection, private ack.\"\"\"\n+\"\"\"HTML evidence graph: byte fidelity, resume, and Evidence-only pages.\"\"\"\n \n from __future__ import annotations\n \n@@ -86,55 +86,10 @@ def test_html_evidence_pages_escape_and_link(evidence_store):\n assert patch_resp.headers[\"content-disposition\"].startswith(\"attachment\")\n \n \n-def test_legacy_epoch_projects_from_gitdiscovery_without_evidence(evidence_store):\n- oid = \"sha1:\" + \"b\" * 40\n- discovery = c.GitDiscovery(\n- schema_version=1,\n- epoch=3,\n- snapshot_id=\"legacy-snap\",\n- timestamp_ms=1,\n- config_digest=\"d\",\n- initial_snapshot=True,\n- configuration={\"repositories\": [], \"contributors\": {}},\n- repositories=[],\n- observations=[{\n- \"oid\": oid,\n- \"eligible\": False,\n- \"exclusion_reason\": \"before_genesis\",\n- \"first_sources\": [],\n- }, {\n- \"oid\": \"sha1:\" + \"c\" * 40,\n- \"eligible\": True,\n- \"exclusion_reason\": None,\n- \"first_sources\": [],\n- }],\n- commits=[{\n- \"oid\": \"sha1:\" + \"c\" * 40,\n- \"contributor\": \"tommy-mor\",\n- \"message\": \"eligible\",\n- \"patch\": \"diff --git a/x b/x\\n\",\n- }],\n- )\n- emission = c.Emission(\n- epoch=3,\n- timestamp_ms=2,\n- pool_before=\"1\",\n- total_emitted=\"0.1\",\n- pool_after=\"0.9\",\n- decay_rate=\"0.1\",\n- distributions={\"tommy-mor\": \"0.1\"},\n- ranking={\"tommy-mor\": \"1\"},\n- models_used=[],\n- discovery_snapshot_id=\"legacy-snap\",\n- )\n- asyncio.run(c.store.append(discovery))\n- asyncio.run(c.store.append(emission))\n- html = asyncio.run(c.epoch_detail(3)).body.decode()\n- assert \"legacy\" in html.lower()\n- assert \"Single-contributor\" in html or \"no LLM\" in html\n- excluded_id = c.commit_id_for_oid(oid)\n- excluded_html = asyncio.run(c.commit_detail(excluded_id)).body.decode()\n- assert \"legacy evidence unavailable\" in excluded_html.lower()\n+def test_commit_without_evidence_is_not_found(evidence_store):\n+ html = asyncio.run(c.commit_detail(\"c_missing\")).body.decode()\n+ assert \"commit not found\" in html\n+ assert \"legacy\" not in html.lower()\n \n \n def test_ranking_resume_skips_duplicate_provider_calls(evidence_store, monkeypatch):\n@@ -202,6 +157,10 @@ def test_epochs_index_lists_epochs(evidence_store):\n distributions={},\n ranking={},\n models_used=[],\n+ discovery_snapshot_id=\"snap\",\n+ evidence_schema_version=c.EVIDENCE_SCHEMA_VERSION,\n+ ranking_run_id=\"r\",\n+ ranking_event_id=\"e\",\n )))\n html = asyncio.run(c.epochs_index()).body.decode()\n assert \"/epochs/3\" in html\n","role":"user"}],"model":"~x-ai/grok-latest"}