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