{"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[d1ad77cf] Add transparent HTML evidence graph for epochs and rankings.\n\nPersist verbatim commits, comparisons, attempts, and judgments in the ledger, serve them as linkable HTML indexes, and keep ranking resumable across restarts.\n\nCo-authored-by: Cursor \n\nSide A — unified diff (full patch):\ndiff --git a/constitution.py b/constitution.py\nindex a58257e1881b21d1d6fa8e68a3faa222e4f661ef..f819007252f435680b8356fb4da83469b21e33af 100644\n--- a/constitution.py\n+++ b/constitution.py\n@@ -156,6 +156,8 @@ DEFAULT_REPOSITORIES = [\n \"refs\": [\"refs/heads/**\"],\n },\n ]\n+PUBLIC_BASE_URL = os.environ.get(\"PUBLIC_BASE_URL\", \"https://token.slug.social\").rstrip(\"/\")\n+EVIDENCE_SCHEMA_VERSION = 2\n DEFAULT_CONTRIBUTORS = {\n \"tommy-mor\": [\"thmorriss@gmail.com\"],\n \"christopher-whitman\": [\n@@ -206,6 +208,9 @@ class Emission:\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 \n \n @event\n@@ -237,7 +242,299 @@ class GitDiscovery:\n commits: list\n \n \n+@event\n+class Evidence:\n+ \"\"\"Versioned, content-addressed constitutional evidence envelope.\"\"\"\n+ schema_version: int\n+ event_id: str\n+ epoch: int\n+ kind: str\n+ recorded_at_ms: int\n+ previous_event_sha256: str\n+ payload: dict\n+\n+\n store = JsonlStore(JSONL_PATH)\n+_LEDGER_LOCK = asyncio.Lock()\n+\n+\n+# ===========================================================================\n+# §1e. EVIDENCE — content-addressed, append-only, publicly linkable\n+# ===========================================================================\n+#\n+# Every epoch, commit, comparison input, provider attempt, and judgment is\n+# recorded as Evidence. Authoritative payloads keep exact bytes as base64 plus\n+# SHA-256; decoded text is for display only.\n+\n+\n+def _canonical_json(obj) -> bytes:\n+ return json.dumps(obj, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False).encode()\n+\n+\n+def _sha256_hex(data: bytes) -> str:\n+ return hashlib.sha256(data).hexdigest()\n+\n+\n+def _bytes_blob(data: bytes | str) -> dict:\n+ raw = data.encode(\"utf-8\") if isinstance(data, str) else data\n+ return {\n+ \"encoding\": \"base64\",\n+ \"data\": base64.b64encode(raw).decode(\"ascii\"),\n+ \"byte_length\": len(raw),\n+ \"sha256\": _sha256_hex(raw),\n+ \"text\": raw.decode(\"utf-8\", \"replace\"),\n+ }\n+\n+\n+def _decode_blob(blob: dict | None) -> bytes:\n+ if not blob:\n+ return b\"\"\n+ return base64.b64decode(blob[\"data\"].encode(\"ascii\"))\n+\n+\n+def _content_id(prefix: str, material) -> str:\n+ digest = _sha256_hex(_canonical_json(material) if not isinstance(material, bytes) else material)\n+ return f\"{prefix}_{digest}\"\n+\n+\n+def _html_escape(text: str) -> str:\n+ return (\n+ text.replace(\"&\", \"&\")\n+ .replace(\"<\", \"<\")\n+ .replace(\">\", \">\")\n+ .replace('\"', \""\")\n+ )\n+\n+\n+def _evidence_path(kind: str, entity_id: str) -> str:\n+ routes = {\n+ \"epoch\": f\"/epochs/{entity_id}\",\n+ \"commit\": f\"/commits/{entity_id}\",\n+ \"comparison\": f\"/comparisons/{entity_id}\",\n+ \"attempt\": f\"/attempts/{entity_id}\",\n+ \"judgment\": f\"/judgments/{entity_id}\",\n+ \"event\": f\"/events/{entity_id}\",\n+ }\n+ return routes[kind]\n+\n+\n+def _evidence_url(kind: str, entity_id: str) -> str:\n+ return PUBLIC_BASE_URL + _evidence_path(kind, entity_id)\n+\n+\n+def _previous_event_sha256(events: list) -> str:\n+ for event_ in reversed(events):\n+ if isinstance(event_, Evidence):\n+ return event_.event_id.split(\"_\", 1)[-1]\n+ if isinstance(event_, (GitDiscovery, Emission)):\n+ return _sha256_hex(_canonical_json(to_dict(event_)))\n+ return \"0\" * 64\n+\n+\n+def _public_repo_row(repo: dict) -> dict:\n+ \"\"\"Strip credential-bearing clone URLs from published config.\"\"\"\n+ url = repo[\"url\"]\n+ if \"@\" in url and \"://\" in url:\n+ scheme, rest = url.split(\"://\", 1)\n+ url = f\"{scheme}://{rest.split('@', 1)[-1]}\"\n+ return {\"id\": repo[\"id\"], \"url\": url, \"refs\": repo[\"refs\"]}\n+\n+\n+def _ledger_lock_path() -> pathlib.Path:\n+ env = os.environ.get(\"LEDGER_LOCK_PATH\")\n+ if env:\n+ return pathlib.Path(env)\n+ return pathlib.Path(str(store.path) + \".lock\")\n+\n+\n+async def append_evidence(epoch: int, kind: str, payload: dict) -> Evidence:\n+ \"\"\"Durably append one Evidence event under process + file locks.\"\"\"\n+ # Logical identity ignores chain links / wall clock so restarts stay idempotent.\n+ event_id = _content_id(\"ev\", {\n+ \"schema_version\": EVIDENCE_SCHEMA_VERSION,\n+ \"epoch\": epoch,\n+ \"kind\": kind,\n+ \"payload\": payload,\n+ })\n+ async with _LEDGER_LOCK:\n+ lock_path = _ledger_lock_path()\n+ lock_path.parent.mkdir(parents=True, exist_ok=True)\n+ with lock_path.open(\"a+b\") as lock_file:\n+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)\n+ try:\n+ events = store.read()\n+ existing = next(\n+ (\n+ e for e in events\n+ if isinstance(e, Evidence) and e.event_id == event_id\n+ ),\n+ None,\n+ )\n+ if existing:\n+ return existing\n+ recorded_at_ms = int(time.time() * 1000)\n+ previous = _previous_event_sha256(events)\n+ evidence = Evidence(\n+ schema_version=EVIDENCE_SCHEMA_VERSION,\n+ event_id=event_id,\n+ epoch=epoch,\n+ kind=kind,\n+ recorded_at_ms=recorded_at_ms,\n+ previous_event_sha256=previous,\n+ payload=payload,\n+ )\n+\n+ def append_once(current):\n+ if any(\n+ isinstance(e, Evidence) and e.event_id == event_id\n+ for e in current\n+ ):\n+ return None\n+ return evidence\n+\n+ appended = await store.atomic(append_once)\n+ result = appended or next(\n+ e for e in store.read()\n+ if isinstance(e, Evidence) and e.event_id == event_id\n+ )\n+ try:\n+ with open(store.path, \"rb\") as fh:\n+ os.fsync(fh.fileno())\n+ except OSError:\n+ pass\n+ finally:\n+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)\n+ await broadcast_audit(\n+ kind,\n+ f\"{kind}: {payload.get('summary') or event_id[:24]}\",\n+ phase=PROCESS_STATE.get(\"phase\"),\n+ progress=PROCESS_STATE.get(\"progress\"),\n+ evidence_event_id=result.event_id,\n+ evidence_url=_evidence_url(\"event\", result.event_id),\n+ )\n+ return result\n+\n+\n+def evidence_by_kind(kind: str | None = None) -> list[Evidence]:\n+ rows = [e for e in store.read() if isinstance(e, Evidence)]\n+ if kind is None:\n+ return rows\n+ return [e for e in rows if e.kind == kind]\n+\n+\n+def find_evidence(event_id: str) -> Evidence | None:\n+ return next(\n+ (e for e in store.read() if isinstance(e, Evidence) and e.event_id == event_id),\n+ None,\n+ )\n+\n+\n+def find_evidence_payload(kind: str, key: str, value: str) -> Evidence | None:\n+ for e in evidence_by_kind(kind):\n+ if e.payload.get(key) == value:\n+ return e\n+ return None\n+\n+\n+def commit_id_for_oid(oid: str) -> str:\n+ return _content_id(\"c\", {\"oid\": oid})\n+\n+\n+def comparison_id_for(material: dict) -> str:\n+ return _content_id(\"cmp\", material)\n+\n+\n+def attempt_id_for(comparison_id: str, model_id: str, attempt_number: int) -> str:\n+ return _content_id(\"att\", {\n+ \"comparison_id\": comparison_id,\n+ \"model_id\": model_id,\n+ \"attempt_number\": attempt_number,\n+ })\n+\n+\n+def judgment_id_for(material: dict) -> str:\n+ return _content_id(\"jud\", material)\n+\n+\n+def _blob_text(blob) -> str:\n+ if blob is None:\n+ return \"\"\n+ if isinstance(blob, str):\n+ return blob\n+ if isinstance(blob, dict):\n+ if isinstance(blob.get(\"text\"), str):\n+ return blob[\"text\"]\n+ try:\n+ return _decode_blob(blob).decode(\"utf-8\", \"replace\")\n+ except Exception:\n+ return \"\"\n+ return str(blob)\n+\n+\n+def _discovery_for_epoch(epoch: int) -> GitDiscovery | None:\n+ return next(\n+ (\n+ e for e in store.read()\n+ if isinstance(e, GitDiscovery) and e.epoch == epoch\n+ ),\n+ None,\n+ )\n+\n+\n+def _emission_for_epoch(epoch: int) -> Emission | None:\n+ return next(\n+ (\n+ e for e in store.read()\n+ if isinstance(e, Emission) and e.epoch == epoch\n+ ),\n+ None,\n+ )\n+\n+\n+def _epochs_in_ledger() -> list[int]:\n+ epochs: set[int] = set()\n+ for e in store.read():\n+ if isinstance(e, (GitDiscovery, Emission, Evidence)):\n+ epochs.add(e.epoch)\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+Return ONLY a JSON object: {{\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}}\n+\n+Side A — commit messages:\n+{side_a['message']}\n+\n+Side A — unified diffs (full patches):\n+{side_a['diff']}\n+\n+Side B — commit messages:\n+{side_b['message']}\n+\n+Side B — unified diffs (full patches):\n+{side_b['diff']}\"\"\"\n \n \n # ===========================================================================\n@@ -565,45 +862,126 @@ async def fetch_top_models(n=3):\n \n \n def _retry_llm_pairwise(exc: BaseException) -> bool:\n- if isinstance(exc, (json.JSONDecodeError, KeyError, IndexError, TypeError)):\n+ if isinstance(exc, (json.JSONDecodeError, KeyError, IndexError, TypeError, ValueError)):\n return True\n if isinstance(exc, httpx.HTTPStatusError):\n return exc.response.status_code in (408, 425, 429, 500, 502, 503, 504)\n return isinstance(exc, httpx.RequestError)\n \n \n-@retry(\n- retry=retry_if_exception(_retry_llm_pairwise),\n- stop=stop_after_attempt(6),\n- wait=wait_exponential(multiplier=1, min=1, max=120),\n- reraise=True,\n-)\n-async def llm_pairwise_compare(model_id, side_a, side_b):\n- prompt = 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-Return ONLY a JSON object: {{\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}}\n+def _retry_wait_seconds(attempt_number: int) -> float:\n+ return min(120.0, float(2 ** (attempt_number - 1)))\n \n-Side A — commit messages:\n-{side_a['message']}\n \n-Side A — unified diffs (full patches):\n-{side_a['diff']}\n-\n-Side B — commit messages:\n-{side_b['message']}\n-\n-Side B — unified diffs (full patches):\n-{side_b['diff']}\"\"\"\n-\n- async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=30.0)) as client:\n- resp = await client.post(\n- f\"{OPENROUTER_BASE_URL}/api/v1/chat/completions\",\n- headers={\"Authorization\": f\"Bearer {OPENROUTER_API_KEY}\"},\n- json={\"model\": model_id, \"messages\": [{\"role\": \"user\", \"content\": prompt}]},\n+async def llm_pairwise_compare(\n+ model_id,\n+ side_a,\n+ side_b,\n+ *,\n+ epoch: int | None = None,\n+ comparison_id: str | None = None,\n+ persist: bool = False,\n+):\n+ \"\"\"Pairwise LLM compare. When persist=True, every attempt is ledger evidence.\"\"\"\n+ prompt = build_pairwise_prompt(side_a, side_b)\n+ request_obj = {\n+ \"model\": model_id,\n+ \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n+ }\n+ request_bytes = _canonical_json(request_obj)\n+ max_attempts = 6\n+ last_error = None\n+ for attempt_number in range(1, max_attempts + 1):\n+ attempt_id = attempt_id_for(\n+ comparison_id or \"ad-hoc\", model_id, attempt_number\n )\n- resp.raise_for_status()\n- content = resp.json()[\"choices\"][0][\"message\"][\"content\"]\n- return json.loads(content)\n+ if persist and epoch is not None and comparison_id is not None:\n+ await append_evidence(epoch, \"llm.attempt_started\", {\n+ \"attempt_id\": attempt_id,\n+ \"comparison_id\": comparison_id,\n+ \"model_id\": model_id,\n+ \"attempt_number\": attempt_number,\n+ \"summary\": f\"{model_id} attempt {attempt_number}\",\n+ \"request\": _bytes_blob(request_bytes),\n+ })\n+ started = time.monotonic()\n+ raw_response = b\"\"\n+ http_status = None\n+ try:\n+ async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=30.0)) as client:\n+ resp = await client.post(\n+ f\"{OPENROUTER_BASE_URL}/api/v1/chat/completions\",\n+ headers={\n+ \"Authorization\": f\"Bearer {OPENROUTER_API_KEY}\",\n+ \"Content-Type\": \"application/json\",\n+ },\n+ content=request_bytes,\n+ )\n+ http_status = resp.status_code\n+ raw_response = resp.content\n+ resp.raise_for_status()\n+ content = resp.json()[\"choices\"][0][\"message\"][\"content\"]\n+ result = json.loads(content)\n+ if result.get(\"winner\") not in {\"A\", \"B\"}:\n+ raise ValueError(\"winner must be A or B\")\n+ ratio_parts = str(result[\"ratio\"]).split(\":\")\n+ if len(ratio_parts) != 2:\n+ raise ValueError(\"ratio must be N:M\")\n+ winner_weight, loser_weight = float(ratio_parts[0]), float(ratio_parts[1])\n+ if winner_weight <= 0 or loser_weight <= 0:\n+ raise ValueError(\"ratio weights must be positive\")\n+ if persist and epoch is not None and comparison_id is not None:\n+ await append_evidence(epoch, \"llm.attempt_finished\", {\n+ \"attempt_id\": attempt_id,\n+ \"comparison_id\": comparison_id,\n+ \"model_id\": model_id,\n+ \"attempt_number\": attempt_number,\n+ \"ok\": True,\n+ \"http_status\": http_status,\n+ \"response\": _bytes_blob(raw_response),\n+ \"duration_ms\": int((time.monotonic() - started) * 1000),\n+ \"summary\": f\"{model_id} attempt {attempt_number} ok\",\n+ })\n+ await append_evidence(epoch, \"llm.judgment\", {\n+ \"judgment_id\": judgment_id_for({\n+ \"attempt_id\": attempt_id,\n+ \"comparison_id\": comparison_id,\n+ \"model_id\": model_id,\n+ \"winner\": result[\"winner\"],\n+ \"ratio\": result[\"ratio\"],\n+ \"explanation\": result.get(\"explanation\", \"\"),\n+ }),\n+ \"attempt_id\": attempt_id,\n+ \"comparison_id\": comparison_id,\n+ \"model_id\": model_id,\n+ \"winner\": result[\"winner\"],\n+ \"ratio\": result[\"ratio\"],\n+ \"explanation\": result.get(\"explanation\", \"\"),\n+ \"summary\": f\"{model_id}: {result['winner']} ({result['ratio']})\",\n+ })\n+ return result\n+ except Exception as exc:\n+ last_error = exc\n+ retryable = _retry_llm_pairwise(exc)\n+ if persist and epoch is not None and comparison_id is not None:\n+ await append_evidence(epoch, \"llm.attempt_finished\", {\n+ \"attempt_id\": attempt_id,\n+ \"comparison_id\": comparison_id,\n+ \"model_id\": model_id,\n+ \"attempt_number\": attempt_number,\n+ \"ok\": False,\n+ \"retryable\": retryable,\n+ \"http_status\": http_status,\n+ \"response\": _bytes_blob(raw_response) if raw_response else None,\n+ \"error\": {\"type\": type(exc).__name__, \"message\": str(exc)},\n+ \"duration_ms\": int((time.monotonic() - started) * 1000),\n+ \"summary\": f\"{model_id} attempt {attempt_number} failed\",\n+ })\n+ if retryable and attempt_number < max_attempts:\n+ await asyncio.sleep(_retry_wait_seconds(attempt_number))\n+ continue\n+ raise\n+ raise RuntimeError(f\"council model failed: {model_id}\") from last_error\n \n \n # ===========================================================================\n@@ -1007,6 +1385,56 @@ def _release_discovery_file_lock(lock_file) -> None:\n lock_file.close()\n \n \n+async def _persist_discovery_evidence(discovery: GitDiscovery) -> None:\n+ \"\"\"Record commit + discovery-completed evidence for a snapshot (idempotent).\"\"\"\n+ public_config = {\n+ \"repositories\": [\n+ _public_repo_row(r) for r in discovery.configuration[\"repositories\"]\n+ ],\n+ \"contributors\": discovery.configuration[\"contributors\"],\n+ }\n+ for commit in discovery.commits:\n+ commit_id = commit_id_for_oid(commit[\"oid\"])\n+ await append_evidence(discovery.epoch, \"git.commit\", {\n+ \"commit_id\": commit_id,\n+ \"oid\": commit[\"oid\"],\n+ \"contributor\": commit[\"contributor\"],\n+ \"message\": _bytes_blob(commit[\"message\"]),\n+ \"patch\": _bytes_blob(commit[\"patch\"] or \"\"),\n+ \"patch_sha256\": commit.get(\"patch_sha256\"),\n+ \"patch_identity\": commit.get(\"patch_identity\"),\n+ \"first_sources\": commit.get(\"first_sources\", []),\n+ \"committer_timestamp_ms\": commit.get(\"committer_timestamp_ms\"),\n+ \"summary\": f\"{commit['oid'][:20]} {commit['contributor']}\",\n+ \"urls\": {\n+ \"commit\": _evidence_url(\"commit\", commit_id),\n+ \"epoch\": _evidence_url(\"epoch\", str(discovery.epoch)),\n+ },\n+ })\n+ await append_evidence(discovery.epoch, \"git.discovery_completed\", {\n+ \"snapshot_id\": discovery.snapshot_id,\n+ \"config_digest\": discovery.config_digest,\n+ \"configuration\": public_config,\n+ \"observation_count\": len(discovery.observations),\n+ \"eligible_count\": len(discovery.commits),\n+ \"commit_ids\": [commit_id_for_oid(c[\"oid\"]) for c in discovery.commits],\n+ \"observations\": [\n+ {\n+ \"oid\": o[\"oid\"],\n+ \"eligible\": o[\"eligible\"],\n+ \"exclusion_reason\": o.get(\"exclusion_reason\"),\n+ \"commit_id\": commit_id_for_oid(o[\"oid\"]),\n+ }\n+ for o in discovery.observations\n+ ],\n+ \"summary\": (\n+ f\"Discovered {len(discovery.observations)} commits; \"\n+ f\"{len(discovery.commits)} eligible\"\n+ ),\n+ \"urls\": {\"epoch\": _evidence_url(\"epoch\", str(discovery.epoch))},\n+ })\n+\n+\n async def discover_repositories(epoch_n: int, boundary_ms: int) -> GitDiscovery:\n async with _DISCOVERY_LOCK:\n lock_file = await asyncio.to_thread(_acquire_discovery_file_lock)\n@@ -1020,6 +1448,7 @@ async def discover_repositories(epoch_n: int, boundary_ms: int) -> GitDiscovery:\n None,\n )\n if existing:\n+ await _persist_discovery_evidence(existing)\n return existing\n candidate = await asyncio.to_thread(\n _build_discovery, epoch_n, boundary_ms, events\n@@ -1034,12 +1463,12 @@ async def discover_repositories(epoch_n: int, boundary_ms: int) -> GitDiscovery:\n return candidate\n \n appended = await store.atomic(append_if_new)\n- if appended:\n- return appended\n- return next(\n+ discovery = appended or next(\n e for e in store.read()\n if isinstance(e, GitDiscovery) and e.epoch == epoch_n\n )\n+ await _persist_discovery_evidence(discovery)\n+ return discovery\n finally:\n await asyncio.to_thread(_release_discovery_file_lock, lock_file)\n \n@@ -1068,6 +1497,9 @@ async def broadcast_audit(\n *,\n progress: int | None = None,\n phase: str | None = None,\n+ evidence_event_id: str | None = None,\n+ evidence_url: str | None = None,\n+ links: dict | None = None,\n ) -> dict:\n global AUDIT_SEQUENCE\n AUDIT_SEQUENCE += 1\n@@ -1083,6 +1515,12 @@ async def broadcast_audit(\n \"message\": message,\n **PROCESS_STATE,\n }\n+ if evidence_event_id:\n+ payload[\"evidence_event_id\"] = evidence_event_id\n+ if evidence_url:\n+ payload[\"evidence_url\"] = evidence_url\n+ if links:\n+ payload[\"links\"] = links\n AUDIT_HISTORY.append(payload)\n del AUDIT_HISTORY[:-200]\n wire = _sse_event(\"audit\", payload)\n@@ -1097,27 +1535,91 @@ async def broadcast_js(js: str):\n await queue.put(js)\n \n \n-async def rank_commits(commits: list[dict]):\n+def _author_side_for_llm(author: str, author_commits: dict) -> dict:\n+ cs = author_commits[author]\n+ return {\n+ \"message\": \"\\n\".join(f\"[{c['sha']}] {c['message']}\" for c in cs),\n+ \"diff\": \"\\n\\n\".join(f\"=== {c['sha']} ===\\n{c['diff']}\" for c in cs),\n+ \"commit_ids\": [c[\"commit_id\"] for c in cs],\n+ \"contributor\": author,\n+ }\n+\n+\n+def _find_judgment(comparison_id: str, model_id: str) -> dict | None:\n+ for e in evidence_by_kind(\"llm.judgment\"):\n+ p = e.payload\n+ if p.get(\"comparison_id\") == comparison_id and p.get(\"model_id\") == model_id:\n+ return p\n+ return None\n+\n+\n+def _find_ranking_models(ranking_run_id: str) -> list[str] | None:\n+ for e in evidence_by_kind(\"ranking.started\"):\n+ if e.payload.get(\"ranking_run_id\") == ranking_run_id:\n+ models = e.payload.get(\"models\")\n+ if isinstance(models, list) and models:\n+ return [str(m) for m in models]\n+ return None\n+\n+\n+async def rank_commits(commits: list[dict], *, epoch: int = -1):\n if not commits:\n- return {}, []\n+ return {}, [], {\"ranking_run_id\": \"\", \"ranking_event_id\": \"\"}\n \n+ commit_ids = sorted(commit_id_for_oid(row[\"oid\"]) for row in commits)\n+ ranking_run_id = _content_id(\"rank\", {\n+ \"epoch\": epoch,\n+ \"commit_ids\": commit_ids,\n+ })\n contributors = sorted(set(c[\"contributor\"] for c in commits))\n+\n if len(contributors) == 1:\n+ await append_evidence(epoch, \"ranking.started\", {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"commit_ids\": commit_ids,\n+ \"contributors\": contributors,\n+ \"models\": [],\n+ \"summary\": f\"ranking epoch {epoch}: single contributor\",\n+ })\n+ ranking = {contributors[0]: Decimal(\"1\")}\n+ completed = await append_evidence(epoch, \"ranking.completed\", {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"models\": [],\n+ \"ranking\": {contributors[0]: \"1\"},\n+ \"judgment_ids\": [],\n+ \"summary\": f\"Only {contributors[0]} is eligible; rank is 1.0\",\n+ })\n await broadcast_audit(\n \"ranking\",\n f\"Only {contributors[0]} is eligible; rank is 1.0\",\n progress=90,\n phase=\"finalizing\",\n+ evidence_event_id=completed.event_id,\n+ evidence_url=_evidence_url(\"event\", completed.event_id),\n+ links={\"epoch\": _evidence_url(\"epoch\", str(epoch))},\n )\n- return {contributors[0]: Decimal(\"1\")}, []\n+ return ranking, [], {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"ranking_event_id\": completed.event_id,\n+ }\n+\n if not (OPENROUTER_API_KEY or \"\").strip():\n raise RuntimeError(\n \"OPENROUTER_API_KEY is required when multiple contributors need ranking\"\n )\n \n- models = await fetch_top_models(n=3)\n+ models = _find_ranking_models(ranking_run_id)\n+ if models is None:\n+ models = await fetch_top_models(n=3)\n if not models:\n raise RuntimeError(\"no council models available for contributor ranking\")\n+ await append_evidence(epoch, \"ranking.started\", {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"commit_ids\": commit_ids,\n+ \"contributors\": contributors,\n+ \"models\": models,\n+ \"summary\": f\"Council selected: {', '.join(models)}\",\n+ })\n await broadcast_audit(\n \"council\",\n f\"Council selected: {', '.join(models)}\",\n@@ -1129,30 +1631,55 @@ async def rank_commits(commits: list[dict]):\n ]))\n \n authors = contributors\n- author_idx = {a: i for i, a in enumerate(authors)}\n-\n author_commits = {a: [] for a in authors}\n- for c in sorted(commits, key=lambda row: row[\"oid\"]):\n- author_commits[c[\"contributor\"]].append({\n- \"message\": c[\"message\"],\n- \"sha\": c[\"oid\"].split(\":\", 1)[1][:8],\n- \"diff\": c[\"patch\"],\n+ for row in sorted(commits, key=lambda r: r[\"oid\"]):\n+ author_commits[row[\"contributor\"]].append({\n+ \"message\": row[\"message\"],\n+ \"sha\": row[\"oid\"].split(\":\", 1)[1][:8],\n+ \"diff\": row[\"patch\"],\n+ \"commit_id\": commit_id_for_oid(row[\"oid\"]),\n })\n \n- # TODO do we want ot coagulate the commits into a single block? or rank the many commits\n- def author_side_for_llm(author):\n- cs = author_commits[author]\n- return {\n- \"message\": \"\\n\".join(f\"[{c['sha']}] {c['message']}\" for c in cs),\n- \"diff\": \"\\n\\n\".join(f\"=== {c['sha']} ===\\n{c['diff']}\" for c in cs),\n- }\n+ judgment_ids: list[str] = []\n \n async def compare_fn(i, j):\n a1, a2 = authors[i], authors[j]\n+ side_a = _author_side_for_llm(a1, author_commits)\n+ side_b = _author_side_for_llm(a2, author_commits)\n+ prompt = build_pairwise_prompt(side_a, side_b)\n+ comparison_material = {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"side_a\": {\n+ \"contributor\": a1,\n+ \"commit_ids\": side_a[\"commit_ids\"],\n+ \"message\": _bytes_blob(side_a[\"message\"]),\n+ \"diff\": _bytes_blob(side_a[\"diff\"]),\n+ },\n+ \"side_b\": {\n+ \"contributor\": a2,\n+ \"commit_ids\": side_b[\"commit_ids\"],\n+ \"message\": _bytes_blob(side_b[\"message\"]),\n+ \"diff\": _bytes_blob(side_b[\"diff\"]),\n+ },\n+ \"prompt\": _bytes_blob(prompt),\n+ }\n+ comparison_id = comparison_id_for(comparison_material)\n+ comparison_material = {\n+ **comparison_material,\n+ \"comparison_id\": comparison_id,\n+ \"summary\": f\"Comparing {a1} with {a2}\",\n+ }\n+ cmp_ev = await append_evidence(epoch, \"comparison.input\", comparison_material)\n await broadcast_audit(\n \"comparison\",\n f\"Comparing {a1} with {a2}\",\n phase=\"ranking\",\n+ evidence_event_id=cmp_ev.event_id,\n+ evidence_url=_evidence_url(\"comparison\", comparison_id),\n+ links={\n+ \"comparison\": _evidence_url(\"comparison\", comparison_id),\n+ \"epoch\": _evidence_url(\"epoch\", str(epoch)),\n+ },\n )\n await broadcast_js(exec_event(Three[Selector(\"#emission-status\")][MORPH][\n [\"div#emission-status\", f\"Comparing {a1} vs {a2}…\"]\n@@ -1160,19 +1687,46 @@ async def rank_commits(commits: list[dict]):\n results = []\n for model in models:\n try:\n- result = await llm_pairwise_compare(model, author_side_for_llm(a1), author_side_for_llm(a2))\n- if result[\"winner\"] not in {\"A\", \"B\"}:\n- raise ValueError(\"winner must be A or B\")\n+ existing = _find_judgment(comparison_id, model)\n+ if existing:\n+ result = {\n+ \"winner\": existing[\"winner\"],\n+ \"ratio\": existing[\"ratio\"],\n+ \"explanation\": existing.get(\"explanation\", \"\"),\n+ }\n+ judgment_ids.append(existing[\"judgment_id\"])\n+ else:\n+ result = await llm_pairwise_compare(\n+ model,\n+ side_a,\n+ side_b,\n+ epoch=epoch,\n+ comparison_id=comparison_id,\n+ persist=True,\n+ )\n+ judgment = _find_judgment(comparison_id, model)\n+ if judgment:\n+ judgment_ids.append(judgment[\"judgment_id\"])\n w, l = (i, j) if result[\"winner\"] == \"A\" else (j, i)\n ratio = result[\"ratio\"].split(\":\")\n winner_weight, loser_weight = float(ratio[0]), float(ratio[1])\n- if winner_weight <= 0 or loser_weight <= 0:\n- raise ValueError(\"ratio weights must be positive\")\n results.append((w, l, winner_weight, loser_weight))\n+ jud_id = (existing or _find_judgment(comparison_id, model) or {}).get(\n+ \"judgment_id\"\n+ )\n await broadcast_audit(\n \"vote\",\n f\"{model}: {authors[w]} over {authors[l]} ({result['ratio']})\",\n phase=\"ranking\",\n+ evidence_url=(\n+ _evidence_url(\"judgment\", jud_id) if jud_id else None\n+ ),\n+ links={\n+ \"judgment\": (\n+ _evidence_url(\"judgment\", jud_id) if jud_id else None\n+ ),\n+ \"comparison\": _evidence_url(\"comparison\", comparison_id),\n+ },\n )\n await broadcast_js(exec_event(Three[Selector(\"#emission-log\")][PREPEND][\n [\"div.log-vote\",\n@@ -1212,16 +1766,25 @@ async def rank_commits(commits: list[dict]):\n \n if not pairs:\n ranking = {authors[0]: Decimal(\"1\")} if authors else {}\n- return ranking, models\n-\n- scores = rank_centrality(pairs)\n- ranking = {authors[i]: Decimal(str(scores[i])) for i in range(len(authors))}\n+ else:\n+ scores = rank_centrality(pairs)\n+ ranking = {authors[i]: Decimal(str(scores[i])) for i in range(len(authors))}\n ranking_rows = sorted(ranking.items(), key=lambda x: x[1], reverse=True)\n+ completed = await append_evidence(epoch, \"ranking.completed\", {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"models\": models,\n+ \"ranking\": {a: str(s) for a, s in ranking_rows},\n+ \"judgment_ids\": judgment_ids,\n+ \"summary\": \"Ranking: \" + \", \".join(f\"{a} {s:.4f}\" for a, s in ranking_rows),\n+ })\n await broadcast_audit(\n \"ranking\",\n \"Ranking: \" + \", \".join(f\"{a} {s:.4f}\" for a, s in ranking_rows),\n progress=90,\n phase=\"finalizing\",\n+ evidence_event_id=completed.event_id,\n+ evidence_url=_evidence_url(\"event\", completed.event_id),\n+ links={\"epoch\": _evidence_url(\"epoch\", str(epoch))},\n )\n await broadcast_js(exec_event(Three[Selector(\"#emission-log\")][PREPEND][\n [\"div.log-ranking\",\n@@ -1229,7 +1792,10 @@ async def rank_commits(commits: list[dict]):\n *[[\"span.rank-entry\", f\"{a} {float(s):.3f} \"] for a, s in ranking_rows],\n ]\n ]))\n- return ranking, models\n+ return ranking, models, {\n+ \"ranking_run_id\": ranking_run_id,\n+ \"ranking_event_id\": completed.event_id,\n+ }\n \n \n # ===========================================================================\n@@ -1245,11 +1811,19 @@ def pool_remaining(events: list) -> Decimal:\n \n async def run_emission(epoch_n, boundary_ms):\n PROCESS_STATE[\"running\"] = True\n+ started = await append_evidence(epoch_n, \"epoch.started\", {\n+ \"boundary_ms\": boundary_ms,\n+ \"summary\": f\"Epoch {epoch_n} emission started\",\n+ \"urls\": {\"epoch\": _evidence_url(\"epoch\", str(epoch_n))},\n+ })\n await broadcast_audit(\n \"start\",\n f\"Epoch {epoch_n} emission started\",\n progress=2,\n phase=\"starting\",\n+ evidence_event_id=started.event_id,\n+ evidence_url=_evidence_url(\"epoch\", str(epoch_n)),\n+ links={\"epoch\": _evidence_url(\"epoch\", str(epoch_n))},\n )\n await broadcast_js(exec_event(Three[Selector(\"#emission-log\")][PREPEND][\n [\"div.log-start\", f\"⚡ Epoch {epoch_n} emission started\"]\n@@ -1270,8 +1844,21 @@ async def run_emission(epoch_n, boundary_ms):\n ),\n progress=30,\n phase=\"discovery\",\n+ links={\n+ \"epoch\": _evidence_url(\"epoch\", str(epoch_n)),\n+ **{\n+ f\"commit_{i}\": _evidence_url(\"commit\", commit_id_for_oid(c[\"oid\"]))\n+ for i, c in enumerate(discovery.commits[:12])\n+ if isinstance(c, dict) and c.get(\"oid\")\n+ },\n+ },\n )\n- ranking, models = await rank_commits(discovery.commits)\n+ ranked = await rank_commits(discovery.commits, epoch=epoch_n)\n+ if len(ranked) == 2:\n+ ranking, models = ranked\n+ ranking_info = {}\n+ else:\n+ ranking, models, ranking_info = ranked\n \n def make_emission(events):\n if epoch_n in {e.epoch for e in events if isinstance(e, Emission)}:\n@@ -1304,6 +1891,9 @@ async def run_emission(epoch_n, boundary_ms):\n distributions={a: str(amount) for a, amount in distributions.items()},\n ranking={a: str(s) for a, s in normalized_ranking.items()},\n models_used=models,\n+ evidence_schema_version=EVIDENCE_SCHEMA_VERSION,\n+ ranking_run_id=ranking_info.get(\"ranking_run_id\", \"\"),\n+ ranking_event_id=ranking_info.get(\"ranking_event_id\", \"\"),\n )\n \n entry = await store.atomic(make_emission)\n@@ -1392,9 +1982,71 @@ async def epoch_loop():\n # §8. API — 2-line read forwards + computed endpoints\n # ===========================================================================\n \n+def _evidence_summary(event_: Evidence) -> dict:\n+ payload = event_.payload\n+ summary = {\n+ \"type\": \"evidence\",\n+ \"schema_version\": event_.schema_version,\n+ \"event_id\": event_.event_id,\n+ \"epoch\": event_.epoch,\n+ \"kind\": event_.kind,\n+ \"recorded_at_ms\": event_.recorded_at_ms,\n+ \"previous_event_sha256\": event_.previous_event_sha256,\n+ \"summary\": payload.get(\"summary\"),\n+ \"urls\": {\"event\": _evidence_url(\"event\", event_.event_id)},\n+ }\n+ for key in (\n+ \"commit_id\", \"comparison_id\", \"attempt_id\", \"judgment_id\",\n+ \"ranking_run_id\", \"snapshot_id\", \"oid\", \"model_id\",\n+ ):\n+ if key in payload:\n+ summary[key] = payload[key]\n+ return summary\n+\n+\n+def _strip_heavy_fields(obj: dict) -> dict:\n+ \"\"\"Bounded listing: drop multi-megabyte blobs unless full=1.\"\"\"\n+ out = {}\n+ for key, value in obj.items():\n+ if key in {\"patch\", \"message\", \"prompt\", \"request\", \"response\", \"diff\"}:\n+ if isinstance(value, dict) and \"sha256\" in value:\n+ out[key] = {\n+ \"sha256\": value[\"sha256\"],\n+ \"byte_length\": value.get(\"byte_length\"),\n+ \"encoding\": value.get(\"encoding\"),\n+ }\n+ elif isinstance(value, str) and len(value) > 256:\n+ out[key] = value[:256] + \"…\"\n+ else:\n+ out[key] = value\n+ elif key == \"commits\" and isinstance(value, list):\n+ out[key] = [\n+ {\n+ k: v for k, v in row.items()\n+ if k != \"patch\"\n+ } | (\n+ {\"patch_sha256\": row.get(\"patch_sha256\")}\n+ if isinstance(row, dict) else {}\n+ )\n+ for row in value\n+ ]\n+ else:\n+ out[key] = value\n+ return out\n+\n+\n @app.get(\"/api/ledger\")\n-async def get_ledger(offset: int = 0, limit: int = 100):\n- return [to_dict(e) for e in store.read()[offset:offset + limit]]\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+ limit = max(1, min(limit, 500))\n+ rows = []\n+ for e in store.read()[offset:offset + limit]:\n+ if isinstance(e, Evidence) and not full:\n+ rows.append(_evidence_summary(e))\n+ else:\n+ d = to_dict(e)\n+ rows.append(d if full else _strip_heavy_fields(d))\n+ return rows\n \n \n @app.get(\"/api/epoch\")\n@@ -1555,6 +2207,582 @@ async def sse_stream(request: Request):\n )\n \n \n+# ===========================================================================\n+# §9b. EVIDENCE HTML — pure indexes/details (no JSON evidence APIs)\n+# ===========================================================================\n+\n+def _a(href: str, label: str) -> list:\n+ return [\"a\", {\"href\": href}, label]\n+\n+\n+def _pre_blob(text: str) -> list:\n+ return [\"pre.blob\", text if text else \"(empty)\"]\n+\n+\n+def _evidence_page(title: str, body: list) -> HTMLResponse:\n+ return HTMLResponse(render([\"html\",\n+ [\"head\",\n+ [\"meta\", {\"charset\": \"utf-8\"}],\n+ [\"meta\", {\"name\": \"viewport\", \"content\": \"width=device-width, initial-scale=1\"}],\n+ [\"title\", title],\n+ [\"style\", RawContent(_WATCH_CSS)],\n+ ],\n+ [\"body.evidence-doc\",\n+ [\"main\", body],\n+ [\"script\", RawContent(_FORM_INTERCEPT_JS)],\n+ ],\n+ ]))\n+\n+\n+def _evidence_nav(*extra: list) -> list:\n+ crumbs = [\n+ _a(\"/\", \"constitution\"), \" · \",\n+ _a(\"/epochs\", \"epochs\"), \" · \",\n+ _a(\"/watch\", \"watch\"),\n+ ]\n+ for item in extra:\n+ crumbs.extend([\" · \", item])\n+ return [\"p.note\", *crumbs]\n+\n+\n+def _dl_rows(rows: list[tuple[str, object]]) -> list:\n+ items = []\n+ for key, value in rows:\n+ if value is None or value == \"\":\n+ continue\n+ items.append([\"div.kv\",\n+ [\"span.k\", key],\n+ [\"span.v\", value if isinstance(value, list) else str(value)],\n+ ])\n+ return [\"div.kv-list\", *items] if items else [\"p.note\", \"(none)\"]\n+\n+\n+def _link_list(pairs: list[tuple[str, str]]) -> list:\n+ if not pairs:\n+ return [\"p.note\", \"(none)\"]\n+ out: list = [\"ul\"]\n+ for label, href in pairs:\n+ out.append([\"li\", _a(href, label)])\n+ return out\n+\n+\n+@app.get(\"/epochs\")\n+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+ )\n+ detail = []\n+ if discovery:\n+ detail.append(\n+ f\"{len(discovery.commits)} eligible / \"\n+ f\"{len(discovery.observations)} 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+ \", \".join(detail) if detail else \"recorded\",\n+ ])\n+ return _evidence_page(\"epochs\", [\n+ _evidence_nav(),\n+ [\"div.eyebrow\", \"transparent constitutional evidence\"],\n+ [\"h1\", \"epochs\"],\n+ [\"p\", \"Each epoch indexes discovery, commits, comparisons, judgments, ranking, and emission.\"],\n+ [\"ul\", *rows] if rows else [\"p.note\", \"No epochs in the ledger yet.\"],\n+ ])\n+\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+ 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+ 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+ comparison_links = [\n+ (\n+ e.payload.get(\"summary\") or e.payload.get(\"comparison_id\", e.event_id),\n+ _evidence_path(\"comparison\", e.payload[\"comparison_id\"]),\n+ )\n+ for e in comparison_evs\n+ if e.payload.get(\"comparison_id\")\n+ ]\n+ judgment_links = [\n+ (\n+ e.payload.get(\"summary\") or e.payload.get(\"judgment_id\", e.event_id),\n+ _evidence_path(\"judgment\", e.payload[\"judgment_id\"]),\n+ )\n+ for e in judgment_evs\n+ if e.payload.get(\"judgment_id\")\n+ ]\n+ event_links = [\n+ (f\"{e.kind} · {e.event_id[:28]}\", _evidence_path(\"event\", e.event_id))\n+ for e in evidence_rows\n+ ]\n+\n+ excluded = []\n+ if discovery:\n+ for obs in discovery.observations:\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+\n+ ranking_nodes: list = []\n+ if ranking_completed:\n+ ranking_nodes = [\n+ [\"p\", ranking_completed.payload.get(\"summary\") or \"ranking completed\"],\n+ _dl_rows([\n+ (\"ranking_run_id\", ranking_completed.payload.get(\"ranking_run_id\")),\n+ (\"models\", \", \".join(ranking_completed.payload.get(\"models\") or [])),\n+ (\"event\", _a(\n+ _evidence_path(\"event\", ranking_completed.event_id),\n+ ranking_completed.event_id,\n+ )),\n+ ]),\n+ [\"pre.blob\", json.dumps(\n+ ranking_completed.payload.get(\"ranking\") or {},\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+ 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+\n+ if emission:\n+ emission_node = _dl_rows([\n+ (\"total_emitted\", emission.total_emitted),\n+ (\"pool_before\", emission.pool_before),\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+ (\"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+\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+ 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+ ]),\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+ [\"h2\", \"excluded observations\"],\n+ [\"ul\", *excluded] if excluded else [\"p.note\", \"(none)\"],\n+ [\"h2\", \"comparisons\"],\n+ _link_list(comparison_links) if comparison_links else [\"p.note\", no_comparisons_note],\n+ [\"h2\", \"judgments\"],\n+ _link_list(judgment_links) if judgment_links else [\"p.note\", \"No judgments recorded.\"],\n+ [\"h2\", \"ranking\"],\n+ *ranking_nodes,\n+ [\"h2\", \"emission\"],\n+ emission_node,\n+ [\"h2\", \"evidence events\"],\n+ _link_list(event_links) if event_links else [\"p.note\", \"(none)\"],\n+ ])\n+ return _evidence_page(f\"epoch {epoch}\", body)\n+\n+\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+ (\"commit_id\", commit_id),\n+ (\"oid\", oid),\n+ (\"contributor\", 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+ [\"p\", _a(f\"/commits/{commit_id}/patch\", \"download patch\")],\n+ [\"h2\", \"message\"],\n+ _pre_blob(message),\n+ [\"h2\", \"patch\"],\n+ _pre_blob(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+ return Response(\n+ content=raw,\n+ media_type=\"text/plain; charset=utf-8\",\n+ headers={\n+ \"Content-Disposition\": f'attachment; filename=\"{commit_id}.patch\"'\n+ },\n+ )\n+\n+\n+@app.get(\"/comparisons/{comparison_id}\")\n+async def comparison_detail(comparison_id: str):\n+ ev = find_evidence_payload(\"comparison.input\", \"comparison_id\", comparison_id)\n+ if not ev:\n+ return _evidence_page(\"comparison not found\", [\n+ _evidence_nav(),\n+ [\"h1\", \"comparison not found\"],\n+ [\"p\", comparison_id],\n+ ])\n+ p = ev.payload\n+ side_a = p.get(\"side_a\") or {}\n+ side_b = p.get(\"side_b\") or {}\n+ attempt_links = []\n+ for a in evidence_by_kind(\"llm.attempt_started\"):\n+ if a.payload.get(\"comparison_id\") == comparison_id:\n+ aid = a.payload.get(\"attempt_id\")\n+ if aid:\n+ attempt_links.append((\n+ f\"{a.payload.get('model_id')} #{a.payload.get('attempt_number')}\",\n+ _evidence_path(\"attempt\", aid),\n+ ))\n+ judgment_links = []\n+ for j in evidence_by_kind(\"llm.judgment\"):\n+ if j.payload.get(\"comparison_id\") == comparison_id:\n+ jid = j.payload.get(\"judgment_id\")\n+ if jid:\n+ judgment_links.append((\n+ j.payload.get(\"summary\") or jid,\n+ _evidence_path(\"judgment\", jid),\n+ ))\n+ commit_links = []\n+ for cid in (side_a.get(\"commit_ids\") or []) + (side_b.get(\"commit_ids\") or []):\n+ commit_links.append((cid, _evidence_path(\"commit\", cid)))\n+ return _evidence_page(f\"comparison {comparison_id[:24]}\", [\n+ _evidence_nav(_a(_evidence_path(\"epoch\", str(ev.epoch)), f\"epoch {ev.epoch}\")),\n+ [\"div.eyebrow\", \"comparison\"],\n+ [\"h1\", comparison_id],\n+ _dl_rows([\n+ (\"summary\", p.get(\"summary\")),\n+ (\"ranking_run_id\", p.get(\"ranking_run_id\")),\n+ (\"evidence_event\", _a(_evidence_path(\"event\", ev.event_id), ev.event_id)),\n+ (\"side_a\", side_a.get(\"contributor\")),\n+ (\"side_b\", side_b.get(\"contributor\")),\n+ ]),\n+ [\"p\", _a(f\"/comparisons/{comparison_id}/prompt\", \"download prompt\")],\n+ [\"h2\", \"commits\"],\n+ _link_list(commit_links),\n+ [\"h2\", \"attempts\"],\n+ _link_list(attempt_links),\n+ [\"h2\", \"judgments\"],\n+ _link_list(judgment_links),\n+ [\"h2\", \"prompt\"],\n+ _pre_blob(_blob_text(p.get(\"prompt\"))),\n+ [\"h2\", f\"side A — {side_a.get('contributor', '?')}\"],\n+ [\"h3\", \"message\"],\n+ _pre_blob(_blob_text(side_a.get(\"message\"))),\n+ [\"h3\", \"diff\"],\n+ _pre_blob(_blob_text(side_a.get(\"diff\"))),\n+ [\"h2\", f\"side B — {side_b.get('contributor', '?')}\"],\n+ [\"h3\", \"message\"],\n+ _pre_blob(_blob_text(side_b.get(\"message\"))),\n+ [\"h3\", \"diff\"],\n+ _pre_blob(_blob_text(side_b.get(\"diff\"))),\n+ ])\n+\n+\n+@app.get(\"/comparisons/{comparison_id}/prompt\")\n+async def comparison_prompt_download(comparison_id: str):\n+ ev = find_evidence_payload(\"comparison.input\", \"comparison_id\", comparison_id)\n+ if not ev:\n+ return PlainTextResponse(\"not found\", status_code=404)\n+ raw = _decode_blob(ev.payload.get(\"prompt\")) or _blob_text(ev.payload.get(\"prompt\")).encode()\n+ return Response(\n+ content=raw,\n+ media_type=\"text/plain; charset=utf-8\",\n+ headers={\n+ \"Content-Disposition\": f'attachment; filename=\"{comparison_id}.prompt.txt\"'\n+ },\n+ )\n+\n+\n+@app.get(\"/attempts/{attempt_id}\")\n+async def attempt_detail(attempt_id: str):\n+ started = find_evidence_payload(\"llm.attempt_started\", \"attempt_id\", attempt_id)\n+ finished = find_evidence_payload(\"llm.attempt_finished\", \"attempt_id\", attempt_id)\n+ if not started and not finished:\n+ return _evidence_page(\"attempt not found\", [\n+ _evidence_nav(),\n+ [\"h1\", \"attempt not found\"],\n+ [\"p\", attempt_id],\n+ ])\n+ base = started or finished\n+ assert base is not None\n+ p = {**(started.payload if started else {}), **(finished.payload if finished else {})}\n+ judgment = None\n+ for j in evidence_by_kind(\"llm.judgment\"):\n+ if j.payload.get(\"attempt_id\") == attempt_id:\n+ judgment = j\n+ break\n+ comparison_id = p.get(\"comparison_id\")\n+ return _evidence_page(f\"attempt {attempt_id[:24]}\", [\n+ _evidence_nav(\n+ _a(_evidence_path(\"epoch\", str(base.epoch)), f\"epoch {base.epoch}\"),\n+ *(\n+ [_a(_evidence_path(\"comparison\", comparison_id), \"comparison\")]\n+ if comparison_id else []\n+ ),\n+ ),\n+ [\"div.eyebrow\", \"llm attempt\"],\n+ [\"h1\", attempt_id],\n+ _dl_rows([\n+ (\"model_id\", p.get(\"model_id\")),\n+ (\"attempt_number\", p.get(\"attempt_number\")),\n+ (\"ok\", p.get(\"ok\")),\n+ (\"http_status\", p.get(\"http_status\")),\n+ (\"duration_ms\", p.get(\"duration_ms\")),\n+ (\"error\", json.dumps(p[\"error\"]) if p.get(\"error\") else None),\n+ (\"started_event\",\n+ _a(_evidence_path(\"event\", started.event_id), started.event_id)\n+ if started else None),\n+ (\"finished_event\",\n+ _a(_evidence_path(\"event\", finished.event_id), finished.event_id)\n+ if finished else None),\n+ (\"judgment\",\n+ _a(\n+ _evidence_path(\"judgment\", judgment.payload[\"judgment_id\"]),\n+ judgment.payload[\"judgment_id\"],\n+ ) if judgment and judgment.payload.get(\"judgment_id\") else None),\n+ ]),\n+ [\"h2\", \"request\"],\n+ _pre_blob(_blob_text(p.get(\"request\"))),\n+ [\"h2\", \"response\"],\n+ _pre_blob(_blob_text(p.get(\"response\"))),\n+ ])\n+\n+\n+@app.get(\"/judgments/{judgment_id}\")\n+async def judgment_detail(judgment_id: str):\n+ ev = find_evidence_payload(\"llm.judgment\", \"judgment_id\", judgment_id)\n+ if not ev:\n+ return _evidence_page(\"judgment not found\", [\n+ _evidence_nav(),\n+ [\"h1\", \"judgment not found\"],\n+ [\"p\", judgment_id],\n+ ])\n+ p = ev.payload\n+ return _evidence_page(f\"judgment {judgment_id[:24]}\", [\n+ _evidence_nav(\n+ _a(_evidence_path(\"epoch\", str(ev.epoch)), f\"epoch {ev.epoch}\"),\n+ *(\n+ [_a(_evidence_path(\"comparison\", p[\"comparison_id\"]), \"comparison\")]\n+ if p.get(\"comparison_id\") else []\n+ ),\n+ *(\n+ [_a(_evidence_path(\"attempt\", p[\"attempt_id\"]), \"attempt\")]\n+ if p.get(\"attempt_id\") else []\n+ ),\n+ ),\n+ [\"div.eyebrow\", \"judgment\"],\n+ [\"h1\", judgment_id],\n+ _dl_rows([\n+ (\"model_id\", p.get(\"model_id\")),\n+ (\"winner\", p.get(\"winner\")),\n+ (\"ratio\", p.get(\"ratio\")),\n+ (\"evidence_event\", _a(_evidence_path(\"event\", ev.event_id), ev.event_id)),\n+ ]),\n+ [\"h2\", \"explanation\"],\n+ _pre_blob(p.get(\"explanation\") or \"\"),\n+ ])\n+\n+\n+@app.get(\"/events/{event_id}\")\n+async def event_detail(event_id: str):\n+ ev = find_evidence(event_id)\n+ if not ev:\n+ return _evidence_page(\"event not found\", [\n+ _evidence_nav(),\n+ [\"h1\", \"event not found\"],\n+ [\"p\", event_id],\n+ ])\n+ p = ev.payload\n+ related = []\n+ for key, kind in (\n+ (\"commit_id\", \"commit\"),\n+ (\"comparison_id\", \"comparison\"),\n+ (\"attempt_id\", \"attempt\"),\n+ (\"judgment_id\", \"judgment\"),\n+ ):\n+ if p.get(key):\n+ related.append((f\"{key}: {p[key]}\", _evidence_path(kind, p[key])))\n+ related.append((f\"epoch {ev.epoch}\", _evidence_path(\"epoch\", str(ev.epoch))))\n+ display_payload = {}\n+ for key, value in p.items():\n+ if isinstance(value, dict) and value.get(\"encoding\") == \"base64\":\n+ display_payload[key] = {\n+ \"sha256\": value.get(\"sha256\"),\n+ \"byte_length\": value.get(\"byte_length\"),\n+ \"text_preview\": (_blob_text(value)[:2000]\n+ + (\"…\" if value.get(\"byte_length\", 0) > 2000 else \"\")),\n+ }\n+ else:\n+ display_payload[key] = value\n+ return _evidence_page(f\"event {event_id[:24]}\", [\n+ _evidence_nav(_a(_evidence_path(\"epoch\", str(ev.epoch)), f\"epoch {ev.epoch}\")),\n+ [\"div.eyebrow\", ev.kind],\n+ [\"h1\", event_id],\n+ _dl_rows([\n+ (\"kind\", ev.kind),\n+ (\"epoch\", ev.epoch),\n+ (\"recorded_at_ms\", ev.recorded_at_ms),\n+ (\"previous_event_sha256\", ev.previous_event_sha256),\n+ (\"schema_version\", ev.schema_version),\n+ ]),\n+ [\"h2\", \"links\"],\n+ _link_list(related),\n+ [\"h2\", \"payload\"],\n+ _pre_blob(json.dumps(display_payload, indent=2, sort_keys=True, default=str)),\n+ ])\n+\n+\n # ===========================================================================\n # §10. UI — hiccup pages + signed snippet POST handler\n # ===========================================================================\n@@ -1765,6 +2993,12 @@ button:disabled { cursor: default; opacity: .4; }\n .event time, .event-kind { color: var(--meta); font-family: var(--font-code); font-size: 10px; }\n .event-kind { text-transform: uppercase; }\n .event-message { color: var(--prose); font-family: var(--font-prose); }\n+.event-links {\n+ font-family: var(--font-code);\n+ font-size: 10px;\n+ grid-column: 1 / -1;\n+}\n+.event-links a { margin-right: 10px; }\n \n code {\n background: var(--g1);\n@@ -1776,9 +3010,33 @@ code {\n padding: 1px 4px;\n }\n \n+body.evidence-doc { max-width: 840px; }\n+.kv-list { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }\n+.kv { display: grid; gap: 8px; grid-template-columns: 140px 1fr; }\n+.kv .k { color: var(--meta); font-family: var(--font-code); font-size: 11px; }\n+.kv .v { color: var(--prose); word-break: break-word; }\n+pre.blob {\n+ background: var(--g1);\n+ border: var(--bv) solid;\n+ border-color: var(--lo) var(--hi) var(--hi) var(--lo);\n+ color: var(--code-fg);\n+ font-family: var(--font-code);\n+ font-size: 11px;\n+ line-height: 1.45;\n+ margin: 8px 0 16px;\n+ max-height: 70vh;\n+ overflow: auto;\n+ padding: 10px;\n+ white-space: pre-wrap;\n+ word-break: break-word;\n+}\n+ul { padding-left: 1.2em; }\n+li { margin: 4px 0; }\n+\n @media (max-width: 520px) {\n .event { grid-template-columns: 72px 1fr; }\n .event-message { grid-column: 1 / -1; }\n+ .kv { grid-template-columns: 1fr; }\n }\n \"\"\"\n \n@@ -1796,6 +3054,8 @@ def _watch_initial_state() -> dict:\n f\"Epoch {event_.epoch}: observed {len(event_.observations)} commits; \"\n f\"{len(event_.commits)} eligible\"\n ),\n+ \"evidence_url\": _evidence_path(\"epoch\", str(event_.epoch)),\n+ \"links\": {\"epoch\": _evidence_path(\"epoch\", str(event_.epoch))},\n })\n elif isinstance(event_, Emission):\n feed.append({\n@@ -1806,6 +3066,17 @@ def _watch_initial_state() -> dict:\n f\"Epoch {event_.epoch}: emitted {event_.total_emitted} SLG; \"\n f\"ranking {event_.ranking}\"\n ),\n+ \"evidence_url\": _evidence_path(\"epoch\", str(event_.epoch)),\n+ \"links\": {\"epoch\": _evidence_path(\"epoch\", str(event_.epoch))},\n+ })\n+ elif isinstance(event_, Evidence):\n+ feed.append({\n+ \"id\": event_.event_id,\n+ \"timestamp_ms\": event_.recorded_at_ms,\n+ \"kind\": event_.kind,\n+ \"message\": event_.payload.get(\"summary\") or event_.kind,\n+ \"evidence_url\": _evidence_path(\"event\", event_.event_id),\n+ \"links\": {\"epoch\": _evidence_path(\"epoch\", str(event_.epoch))},\n })\n feed.extend(AUDIT_HISTORY)\n return {\n@@ -1852,6 +3123,25 @@ function addEvent(event) {\n message.className = 'event-message';\n message.textContent = event.message;\n row.append(when, kind, message);\n+ const linkPairs = [];\n+ if (event.evidence_url) linkPairs.push(['evidence', event.evidence_url]);\n+ if (event.links && typeof event.links === 'object') {\n+ for (const [label, href] of Object.entries(event.links)) {\n+ if (href) linkPairs.push([label, href]);\n+ }\n+ }\n+ if (linkPairs.length) {\n+ const links = document.createElement('span');\n+ links.className = 'event-links';\n+ for (const [label, href] of linkPairs) {\n+ const a = document.createElement('a');\n+ a.href = href;\n+ a.textContent = label;\n+ links.appendChild(a);\n+ links.appendChild(document.createTextNode(' '));\n+ }\n+ row.appendChild(links);\n+ }\n feed.prepend(row);\n while (feed.children.length > 200) feed.lastElementChild.remove();\n }\n@@ -1957,6 +3247,7 @@ async def watch():\n [\"div#audit-feed\"],\n ],\n [\"p\", [\"a\", {\"href\": \"/\"}, \"← constitution\"], \" · \",\n+ [\"a\", {\"href\": \"/epochs\"}, \"epochs\"], \" · \",\n [\"a\", {\"href\": \"/api/status\"}, \"status JSON\"], \" · \",\n [\"a\", {\"href\": \"/api/ledger\"}, \"ledger\"]],\n ]],\n@@ -1984,6 +3275,7 @@ async def index(request: Request):\n [\"p\", [\"a\", {\"href\": \"/login\"}, \"Login with GitHub to check your emissions\"]],\n [\"p\",\n [\"a\", {\"href\": \"/watch\"}, \"Watch emission process live\"], \" | \",\n+ [\"a\", {\"href\": \"/epochs\"}, \"Epoch evidence\"], \" | \",\n [\"a\", {\"href\": \"/api/epoch\"}, \"Current epoch\"], \" | \",\n [\"a\", {\"href\": \"/api/ledger\"}, \"Full ledger\"], \" | \",\n [\"a\", {\"href\": \"/api/halvening\"}, \"Jubilee countdown\"],\n@@ -2010,6 +3302,7 @@ async def index(request: Request):\n redeem_form,\n [\"p\",\n [\"a\", {\"href\": \"/api/epoch\"}, \"Current epoch\"], \" | \",\n+ [\"a\", {\"href\": \"/epochs\"}, \"Epoch evidence\"], \" | \",\n [\"a\", {\"href\": \"/api/ledger\"}, \"Full ledger\"], \" | \",\n [\"a\", {\"href\": \"/watch\"}, \"Watch emission live\"],\n ],\ndiff --git a/tests/integration.clj b/tests/integration.clj\nindex 06b9715e63966d14311b159cbc7058952bff2fbc..204b0fedfbad57846205cb140c4ed9406b5f8e0e 100644\n--- a/tests/integration.clj\n+++ b/tests/integration.clj\n@@ -404,6 +404,7 @@\n \"ALLOW_TEST_TRIGGERS\" \"1\"\n \"OPENROUTER_BASE_URL\" (str \"http://127.0.0.1:\" or-port)\n \"SLUG_MODEL_RANK_PARENT\" \"\"\n+ \"PUBLIC_BASE_URL\" (str \"http://127.0.0.1:\" server-port)\n \"PATH\" (get (into {} (System/getenv)) \"PATH\" \"\")})\n \n (bind !server (atom nil))\n@@ -506,19 +507,55 @@\n (assert! (>= (:compare-requests or-state) 3) \"at least 3 pairwise LLM calls (2 authors × 3 models)\")\n \n (bind ledger2 (get-json base-url \"/api/ledger\"))\n- (assert! (= 3 (count ledger2))\n- \"ledger has seed, discovery, and emission entries\")\n- (bind discovery-entry (second ledger2))\n- (assert! (= \"gitdiscovery\" (:type discovery-entry))\n+ (assert! (>= (count ledger2) 3)\n+ \"ledger has seed + discovery + emission (+ evidence)\")\n+ (bind discovery-entry\n+ (first (filter #(= \"gitdiscovery\" (:type %)) ledger2)))\n+ (assert! (some? discovery-entry)\n \"discovery is persisted before emission\")\n (assert! (= 2 (count (:repositories discovery-entry)))\n \"discovery records both repositories\")\n (assert! (= 2 (count (:commits discovery-entry)))\n \"discovery admits one contribution from each repository\")\n+ (bind evidence-kinds\n+ (set (keep :kind (filter #(= \"evidence\" (:type %)) ledger2))))\n+ (assert! (contains? evidence-kinds \"git.commit\")\n+ \"evidence includes git.commit\")\n+ (assert! (contains? evidence-kinds \"comparison.input\")\n+ \"evidence includes comparison.input\")\n+ (assert! (contains? evidence-kinds \"llm.judgment\")\n+ \"evidence includes llm.judgment\")\n (bind rank-after (get-json base-url \"/api/ranking\"))\n (assert! (= 1 (:epoch rank-after)) \"latest ranking is epoch 1\")\n \n- ;; 12. kill and restart — prove replay determinism\n+ ;; 11b. HTML evidence indexes are crawlable\n+ (println \"\\nchecking HTML evidence indexes…\")\n+ (bind epochs-html (slurp (str base-url \"/epochs\")))\n+ (assert! (str/includes? epochs-html \"/epochs/1\")\n+ \"epochs index links epoch 1\")\n+ (bind epoch-html (slurp (str base-url \"/epochs/1\")))\n+ (assert! (str/includes? epoch-html \"/commits/\")\n+ \"epoch page links commits\")\n+ (assert! (str/includes? epoch-html \"/comparisons/\")\n+ \"epoch page links comparisons\")\n+ (assert! (str/includes? epoch-html \"/judgments/\")\n+ \"epoch page links judgments\")\n+ (bind commit-href\n+ (second (re-find #\"/commits/(c_[a-f0-9]+)\" epoch-html)))\n+ (assert! (some? commit-href) \"found a commit id on epoch page\")\n+ (bind commit-html (slurp (str base-url \"/commits/\" commit-href)))\n+ (assert! (str/includes? commit-html \"download patch\")\n+ \"commit page offers patch download\")\n+ (bind patch-bytes\n+ (let [tmp (doto (java.io.File/createTempFile \"patch\" \".bin\") .deleteOnExit)\n+ r @(p/process [\"curl\" \"-sS\" \"-o\" (.getAbsolutePath tmp)\n+ (str base-url \"/commits/\" commit-href \"/patch\")]\n+ {:out :string :err :string})]\n+ (assert! (zero? (:exit r)) \"patch download succeeds\")\n+ (slurp tmp)))\n+ (assert! (pos? (count patch-bytes)) \"patch download is non-empty\")\n+\n+ ;; 12. kill and restart — prove replay determinism + no duplicate LLM calls\n (println \"\\nkilling server for replay test…\")\n (.destroyForcibly (:proc server))\n (deref server)\n@@ -533,12 +570,15 @@\n \"restarted server responds to /api/epoch\")\n \n (bind replayed-ledger (get-json base-url \"/api/ledger\"))\n- (assert! (= 3 (count replayed-ledger))\n- \"ledger still has 3 entries after replay\")\n+ (assert! (= (count ledger2) (count replayed-ledger))\n+ \"ledger entry count unchanged after replay\")\n (bind replayed-rank (get-json base-url \"/api/ranking\"))\n (assert! (= (get-in rank-after [:ranking :alice])\n (get-in replayed-rank [:ranking :alice]))\n- \"alice's rank score (epoch 1) identical after replay\"))\n+ \"alice's rank score (epoch 1) identical after replay\")\n+ (bind epoch-html2 (slurp (str base-url \"/epochs/1\")))\n+ (assert! (str/includes? epoch-html2 \"/judgments/\")\n+ \"epoch HTML evidence still linked after restart\"))\n \n (finally\n (when-some [s @!server]\ndiff --git a/tests/test_evidence.py b/tests/test_evidence.py\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..76adcd343eec3a65dec58fda93b068356fb1adcd\n--- /dev/null\n+++ b/tests/test_evidence.py\n@@ -0,0 +1,214 @@\n+\"\"\"HTML evidence graph: byte fidelity, resume, legacy projection, private ack.\"\"\"\n+\n+from __future__ import annotations\n+\n+import asyncio\n+import base64\n+import hashlib\n+import json\n+\n+import pytest\n+\n+import constitution as c\n+\n+\n+@pytest.fixture\n+def evidence_store(tmp_path, monkeypatch):\n+ monkeypatch.setattr(c, \"store\", c.JsonlStore(tmp_path / \"ledger.jsonl\"))\n+ monkeypatch.setattr(c, \"PUBLIC_BASE_URL\", \"http://test.local\")\n+ return tmp_path\n+\n+\n+def test_bytes_blob_roundtrip_crlf_nul_invalid_utf8():\n+ raw = b\"line\\r\\n\\x00\\xff\"\n+ blob = c._bytes_blob(raw)\n+ assert blob[\"encoding\"] == \"base64\"\n+ assert blob[\"byte_length\"] == len(raw)\n+ assert blob[\"sha256\"] == hashlib.sha256(raw).hexdigest()\n+ assert c._decode_blob(blob) == raw\n+ assert \"\\n\"\n+ commit_id = c.commit_id_for_oid(\"sha1:\" + \"a\" * 40)\n+ asyncio.run(c.append_evidence(3, \"git.commit\", {\n+ \"commit_id\": commit_id,\n+ \"oid\": \"sha1:\" + \"a\" * 40,\n+ \"contributor\": \"alice\",\n+ \"message\": c._bytes_blob(\"msg x\"),\n+ \"patch\": c._bytes_blob(patch),\n+ \"summary\": \"commit\",\n+ }))\n+ cmp_id = c.comparison_id_for({\"x\": 1})\n+ asyncio.run(c.append_evidence(3, \"comparison.input\", {\n+ \"comparison_id\": cmp_id,\n+ \"summary\": \"cmp\",\n+ \"prompt\": c._bytes_blob(\"prompt \"),\n+ \"side_a\": {\"contributor\": \"alice\", \"commit_ids\": [commit_id]},\n+ \"side_b\": {\"contributor\": \"bob\", \"commit_ids\": []},\n+ }))\n+ commit_html = asyncio.run(c.commit_detail(commit_id)).body.decode()\n+ assert \"