Side A fixes a real bug (empty external garden index due to bogus parent), cleanly restructures the GitHub resolver into a resolvers/ module with a proper card schema/renderer, and adds targeted unit/integration tests validating the new behavior. Side B is largely infra/deploy plumbing (Dockerfile, fly.toml, CI) plus a sizable dashboard/SSE feature for a separate constitution.py service, which is useful operationally but is less about core product correctness and mixes config, UI, and scattered test tweaks.
constitution · epochs · watch · epoch 3
c_df12ba3b70a8 (tommy-mor) vs c_7a129e904906 (tommy-mor)
download prompt · raw event · cmp_896b07d9b14206
council reasoning
B ships the constitution as a running, auditable production system: Fly/Docker/CI deploy path, expanded real repo/contributor roots, resilient epoch execution, /api/status, and a tested /watch SSE progress UI—foundational lasting infrastructure. A’s /- host-index bugfix, resolvers/ card schema, and GitHub rich rendering are strong product work, but more incremental on slug.social than standing up the live ownership process itself.
Side A fixes a real functional bug by replacing the bogus `https://.` parent lookup for the external garden index with `external_root_host_items`, ensuring external roots are discovered even from implicit child edges, and adds tests for that behavior. It also introduces a reusable resolver architecture (`server/src/resolvers/`), rich GitHub import card rendering via `render_item_body_in_scope`, and integration/unit tests, whereas Side B is primarily deployment and observability infrastructure (Docker, Fly, GitHub Actions, SSE dashboard, status APIs) that improves operations but less directly changes the project's core behavior.
sides
A — c_df12ba3b70a8 (tommy-mor)
message
[23c8134e] Fix /-/ external garden index; resolvers/ + GitHub import cards (#150) * Fix external garden root listing; add resolvers/ with GitHub cards The public and room external index pages queried children of a bogus https://./ parent, so /-/ always looked empty. Collect host-only https roots from all Web items and item_children edges so ghost parents from add_child_edge appear. Move GitHub resolver into server/src/resolvers/ with default_external.rs and a try_render_resolver_item_body hook. Resolver ingests now store slug-github-card fenced JSON; render_item_body_in_scope shows a small GitHub article card (with legacy support for schema-less json fences on github.com URLs). Styling in theme_default.css; agents.md updated. Co-authored-by: tommy <thmorriss@gmail.com> * Vote compare: GitHub cards in columns, layout CSS, tests Pass item_bodies into vote_compare_item_card for linkified tooltips on non-card bodies; clone item_bodies before dropping reducer read guard. Add layout rules so rich cards sit in the grid corners (default + retro). Unit test on vote_compare_item_card; integration GET /vote/compare with ingested slug-github-card bodies. agents.md clarifies compare columns. Co-authored-by: tommy <thmorriss@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
diff preview
diff --git a/agents.md b/agents.md
index 7508234d9b04223d0e64cfe69fedbebd06a256b5..d8b801e454fdf37e7ac6038b91a69f83b0746d59 100644
--- a/agents.md
+++ b/agents.md
@@ -42,7 +42,7 @@ Strict **CSP** that blocks `eval` would break the current app. Other projects ma
- **`VoteComparePost`:** On success returns **`text/javascript`** that **morphs** **`#vote-edge-history-region`** (recomputed **`<ul>`** — ratios match **`left`/`right`** query order, bullets, sorted by strength toward **`left`** then newer) and **`.vote-compare-nav`** (fresh next-pair link). The compare **`GET`** page uses **`layout_full_bleed_chromeless`** (no breadcrumbs, no **`#controls`**, no **`slug-pin-hud`**; **`view-vote-compare-fullscreen`** full-width **`body`**). **`__rpc__`** carries **`form_action: "/ui"`**; **`thread_tag`** and ratio fields come from the same form as **`$form`** holes.
-- **`ResolveExternal`:** GitHub resolver buttons are browser actions through **`POST /ui`**. Success responses morph **`#external-resolver-status`** then redirect to the sanitized shareable **`GET`** page so imported children render through the normal page path; errors morph the same status region. Resolver results are durable system ingests, while cooldown state is RAM-only.
+- **`ResolveExternal`:** GitHub resolver buttons are browser actions through **`POST /ui`**. Success responses morph **`#external-resolver-status`** then redirect to the sanitized shareable **`GET`** page so imported children render through the normal page path; errors morph the same status region. Resolver results are durable system ingests, while cooldown state is RAM-only. Implementation lives under **`server/src/resolvers/`** (GitHub resolver + import card JSON); ontology item pages and the **`GET /vote/compare`** left/right columns use **`render_item_body_in_scope`** in **`server/src/html/mod.rs`**, which calls **`server/src/resolvers/mod.rs::try_render_resolver_item_body`** before falling back to the usual **`<pre>`** linkified view.
- **Garden pin / compare voting:** Cookie **`slug_garden_pin`** via **`set_garden_pin`**. Pairwise UI: **`GET /vote/compare?…`** / **`GET /r/:room_key/vote/compare?…`** (fullscreen **`GET`** page: no HUD; other garden pages). HUD (**`#slug-pin-hud`**): only when **`layout`** passes garden metadata on **`body`**; the label is **`POST /ui`** **`set_garden_pin`** **`clear:true`** (**`slug_ui.js`**), not a permalink to the item.
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 4b0214d18b173cd506d09176104f461dc4c4f208..c9eb8e242072e41fcf70da838bdf02dd4c838db8 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -18,7 +18,7 @@ use crate::{
rpc::{rpc_post_redact, rpc_post_with_bearer, rpc_room_delete},
},
canonical_path::canonicalize_tag,
- external_resolver::resolve_github_children,
+ resolvers::resolve_github_children,
html::vote_compare_post_success_js,
html::{
external_resolver_status_markup, fragment_new_thread_slot, login_to_post_hint_markup,
diff --git a/server/src/external_resolver.rs b/server/src/external_resolver.rs
deleted file mode 100644
index a5812250fed7613950b5417f396f886a55fafccf..0000000000000000000000000000000000000000
--- a/server/src/external_resolver.rs
+++ /dev/null
@@ -1,630 +0,0 @@
-use async_trait::async_trait;
-use serde_json::Value;
-use tokio::sync::oneshot;
-
-use crate::{path_types::ItemId, state::AppState, write_cmd::WriteCmd};
-
-const GITHUB_SYSTEM_PRINCIPAL: &str = "system:github-resolver";
-const GITHUB_RESOLVER_COOLDOWN_MS: i64 = 15_000;
-const GITHUB_MAX_PAGES: usize = 3;
-
-fn now_ms() -> i64 {
- use std::time::{SystemTime, UNIX_EPOCH};
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or_default()
- .as_millis() as i64
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResolvedChild {
- pub url: String,
- pub title: String,
- pub body: Option<String>,
-}
-
-#[async_trait]
-pub trait ExternalResolver: Send + Sync {
- /// e.g. `"github.com"`
- fn domain_match(&self) -> &'static str;
-
- /// Normalizes URLs (e.g. stripping fragments); extend per-domain later.
- fn normalize(&self, path: &str) -> String;
-
- /// Fetches body when missing; GitHub hook lands here in a follow-up.
- async fn fetch_body(&self, item: &ItemId) -> Result<String, String>;
-}
-
-#[derive(Clone)]
-pub struct GitHubResolver {
- client: reqwest::Client,
- api_base_url: String,
- token: Option<String>,
-}
-
-impl GitHubResolver {
- pub fn from_env() -> Self {
- let api_base_url = std::env::var("SLUG_GITHUB_API_BASE_URL")
- .ok()
- .filter(|s| !s.trim().is_empty())
- .unwrap_or_else(|| "https://api.github.com".to_string());
- let token = std::env::var("SLUG_GITHUB_TOKEN")
- .ok()
- .filter(|s| !s.trim().is_empty());
- Self {
- client: reqwest::Client::new(),
- api_base_url: api_base_url.trim_end_matches('/').to_string(),
- token,
- }
- }
-
- pub fn can_resolve_children(&self, item: &ItemId) -> bool {
- github_segments(item).is_some()
- }
-
- pub async fn list_children(&self, item: &ItemId) -> Result<Vec<ResolvedChild>, String> {
- let segments = github_segments(item).ok_or_else(|| "not a GitHub URL".to_string())?;
- match segments.as_slice() {
- [] => Ok(vec![]),
- [owner] => self.list_repos(owner).await,
- [owner, repo] => Ok(github_repo_sections(owner, repo)),
- [owner, repo, section] if section == "issues" => self.list_issues(owner, repo).await,
- [owner, repo, section] if section == "pulls" => self.list_pulls(owner, repo).await,
- [owner, repo, section] if section == "commits" => self.list_commits(owner, repo).await,
- [owner, repo, section] if section == "releases" => {
- self.list_releases(owner, repo).await
- }
- _ => Ok(vec![]),
- }
- }
-
- async fn get_json(&self, path: &str) -> Result<Value, String> {
- let url = format!("{}/{}", self.api_base_url, path.trim_start_matches('/'));
- let mut req = self
- .client
- .get(url)
- .header(reqwest::header::USER_AGENT, "slugsocial-github-resolver");
- if let Some(token) = &self.token {
- req = req.bearer_auth(token);
- }
- let resp = req
- .send()
- .await
- .map_err(|e| format!("GitHub request failed: {e}"))?;
- let status = resp.status();
- if !status.is_success() {
- return Err(format!("GitHub request returned {status}"));
- }
- resp.json::<Value>()
- .await
- .map_err(|e| format!("GitHub response JSON failed: {e}"))
- }
-
- async fn get_json_array_pages(&self, path: &str) -> Result<Vec<Value>, String> {
- let sep = if path.contains('?') { '&' } else { '?' };
- let mut out = Vec::new();
- for page in 1..=GITHUB_MAX_PAGES {
- let value = self.get_json(&format!("{path}{sep}page={page}")).await?;
- let arr = value
- .as_array()
- .ok_or_else(|| "GitHub paged response was not an array".to_string())?;
- let n = arr.len();
- out.extend(arr.iter().cloned());
- if n < 100 {
- break;
- }
- }
- Ok(out)
- }
-
- async fn list_repos(&self, owner: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!(
- "/users/{owner}/repos?per_page=100&sort=updated&type=owner"
- ))
- .await?;
- let mut out = Vec::new();
- for repo in &arr {
- let name = repo
- .get("name")
- .and_then(|v| v.as_str())
- .unwrap_or_default();
- if name.is_empty() {
- continue;
- }
- let full_name = repo
- .get("full_name")
- .and_then(|v| v.as_str())
- .map(|s| s.to_ascii_lowercase())
- .unwrap_or_else(|| format!("{owner}/{name}").to_ascii_lowercase());
- out.push(ResolvedChild {
- url: format!("https://github.com/{full_name}"),
- title: full_name.clone(),
- body: Some(github_repo_body(repo)),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_issues(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!(
- "/repos/{owner}/{repo}/issues?state=open&per_page=100"
- ))
- .await?;
- let mut out = Vec::new();
- for issue in &arr {
- if issue.get("pull_request").is_some() {
- continue;
- }
- let Some(number) = issue.get("number").and_then(|v| v.as_i64()) else {
- continue;
- };
- let title = issue
- .get("title")
- .and_then(|v| v.as_str())
- .unwrap_or("Untitled issue");
- out.push(ResolvedChild {
- url: format!("https://github.com/{owner}/{repo}/issues/{number}"),
- title: format!("#{number} {title}"),
- body: Some(github_issue_body(issue, "issue")),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_pulls(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!(
- "/repos/{owner}/{repo}/pulls?state=open&per_page=100"
- ))
- .await?;
- let mut out = Vec::new();
- for pull in &arr {
- let Some(number) = pull.get("number").and_then(|v| v.as_i64()) else {
- continue;
- };
- let title = pull
- .get("title")
- .and_then(|v| v.as_str())
- .unwrap_or("Untitled pull request");
- out.push(ResolvedChild {
- url: format!("https://github.com/{owner}/{repo}/pulls/{number}"),
- title: format!("#{number} {title}"),
- body: Some(github_issue_body(pull, "pull request")),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_commits(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr = self
- .get_json_array_pages(&format!("/repos/{owner}/{repo}/commits?per_page=100"))
- .await?;
- let mut out = Vec::new();
- for commit in &arr {
- let Some(sha) = github_string(commit, "sha") else {
- continue;
- };
- let short = sha.chars().take(7).collect::<String>();
- let title = commit
- .get("commit")
- .and_then(|c| c.get("message"))
- .and_then(|v| v.as_str())
- .and_then(|m| m.lines().next())
- .filter(|s| !s.trim().is_empty())
- .unwrap_or("commit");
- let url = github_string(commit, "html_url")
- .map(|s| s.to_string())
- .unwrap_or_else(|| format!("https://github.com/{owner}/{repo}/commit/{sha}"));
- out.push(ResolvedChild {
- url,
- title: format!("{short} {title}"),
- body: Some(github_commit_body(commit)),
- });
- }
- out.sort_by(|a, b| a.url.cmp(&b.url));
- Ok(out)
- }
-
- async fn list_releases(&self, owner: &str, repo: &str) -> Result<Vec<ResolvedChild>, String> {
- let arr =
… preview truncated; 60,917 characters omittedB — c_7a129e904906 (tommy-mor)
message
[4e327784] deploy live constitution dashboard Expose auditable progress and event streaming, configure the production roots and runtime, and make tested main-branch commits the deployment authority. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..c9d63a722beba0a0297fc853089332c460ab78dd
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+.git
+.venv
+.hypothesis
+__pycache__
+tests
+*.json
+*.bsp
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..76dcdf82d53177c1e47d86b23a54523239d232a6
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,49 @@
+name: Test and deploy
+
+on:
+ push:
+ branches: [main]
+
+concurrency:
+ group: production
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Run Python tests
+ run: uv run pytest -q
+
+ - name: Install Babashka
+ run: |
+ curl -fsSL https://raw.githubusercontent.com/babashka/babashka/master/install \
+ | sudo bash -s -- --dir /usr/local/bin
+
+ - name: Run process integration tests
+ run: bb TEST.sh
+
+ deploy:
+ needs: test
+ runs-on: ubuntu-latest
+ environment:
+ name: production
+ url: https://token.slug.social
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: superfly/flyctl-actions/setup-flyctl@master
+
+ - name: Deploy to Fly
+ run: flyctl deploy --remote-only
+ env:
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..c9a5c00782371c19ad5ab5c58cf6f5a8ffec0141
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,17 @@
+FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends git ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY pyproject.toml uv.lock ./
+RUN uv sync --frozen --no-install-project
+
+COPY constitution.py ./
+
+ENV PATH="/app/.venv/bin:${PATH}" \
+ PYTHONUNBUFFERED="1"
+
+EXPOSE 8080
+CMD ["python", "constitution.py"]
diff --git a/constitution.py b/constitution.py
index 4bee9f83663ab7fb36db95129b92b64b4ef57258..a58257e1881b21d1d6fa8e68a3faa222e4f661ef 100644
--- a/constitution.py
+++ b/constitution.py
@@ -24,12 +24,12 @@ A daily GitHub Action backs up the JSONL ledger to the same repo.
Run: uv run constitution.py
"""
-from decimal import Decimal, getcontext
+from decimal import Decimal, getcontext, DefaultContext
from datetime import datetime, timezone
from fastapi import FastAPI, Request, Response
from fastapi.responses import PlainTextResponse, HTMLResponse
from starlette.middleware.sessions import SessionMiddleware
-import json, time, os, asyncio, httpx, pathlib, subprocess, hashlib, re, fcntl
+import json, time, os, asyncio, httpx, pathlib, subprocess, hashlib, re, fcntl, base64
import sympy as sp # type: ignore[reportMissingImports]
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
from evaleval import (
@@ -37,6 +37,7 @@ from evaleval import (
exec_event, One, Two, Three, Selector, MORPH, PREPEND,
)
+DefaultContext.prec = 50
getcontext().prec = 50
app = FastAPI()
@@ -129,14 +130,47 @@ OPENROUTER_BASE_URL = os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.
# using the exact same source; their normalized values are committed to every
# discovery event.
DEFAULT_REPOSITORIES = [
+ {
+ "id": "constitution",
+ "url": "https://github.com/sortersocial/constitution.git",
+ "refs": ["refs/heads/**"],
+ },
{
"id": "slug",
- "url": "https://github.com/tommy-mor/slug.git",
+ "url": "https://github.com/sortersocial/slug.git",
+ "refs": ["refs/heads/**"],
+ },
+ {
+ "id": "sorter",
+ "url": "https://github.com/sorterisntonline/sorter.git",
+ "refs": ["refs/heads/**"],
+ },
+ {
+ "id": "sorter2",
+ "url": "https://github.com/sortersocial/sorter2.git",
+ "refs": ["refs/heads/**"],
+ },
+ {
+ "id": "sorter-oldest",
+ "url": "https://github.com/tommy-mor/sorter.git",
"refs": ["refs/heads/**"],
},
]
DEFAULT_CONTRIBUTORS = {
"tommy-mor": ["thmorriss@gmail.com"],
+ "christopher-whitman": [
+ "chris@cwwhitman.com",
+ "7566903+cwwhitman@users.noreply.github.com",
+ ],
+ "jake-chvatal": [
+ "jake+github@uln.industries",
+ "jakechvatal@gmail.com",
+ "jake@isnt.online",
+ ],
+ "lara": ["me@lara.lv"],
+ "nat-reid": ["nathanielreid@gmail.com"],
+ "zod": ["jason.p.mcel@gmail.com", "me@zod.tf"],
+ "jovan": ["jovan@slug.social", "jovan@getcivicai.com"],
}
REPOSITORIES = json.loads(
@@ -147,6 +181,7 @@ CONTRIBUTORS = json.loads(
)
GIT_MIRROR_DIR = pathlib.Path(os.environ.get("GIT_MIRROR_DIR", "/data/git"))
GIT_TIMEOUT_SECONDS = int(os.environ.get("GIT_TIMEOUT_SECONDS", "120"))
+GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
# Council model IDs: slug.social garden rank under this parent (bodies = OpenRouter URLs), then top-up from OpenRouter list.
SLUG_SOCIAL_BASE_URL = os.environ.get("SLUG_SOCIAL_BASE_URL", "https://slug.social").rstrip("/")
@@ -661,20 +696,30 @@ def _git(repo: pathlib.Path | None, *args: str, input_bytes: bytes | None = None
if repo is not None:
command += ["-C", str(repo)]
command += list(args)
+ git_env = {
+ **os.environ,
+ "GIT_CONFIG_NOSYSTEM": "1",
+ "GIT_CONFIG_GLOBAL": os.devnull,
+ "GIT_NO_REPLACE_OBJECTS": "1",
+ "LC_ALL": "C",
+ "TZ": "UTC",
+ }
+ if GITHUB_TOKEN:
+ credential = base64.b64encode(
+ f"x-access-token:{GITHUB_TOKEN}".encode()
+ ).decode()
+ git_env.update({
+ "GIT_CONFIG_COUNT": "1",
+ "GIT_CONFIG_KEY_0": "http.https://github.com/.extraHeader",
+ "GIT_CONFIG_VALUE_0": f"Authorization: Basic {credential}",
+ })
try:
result = subprocess.run(
command,
input=input_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
- env={
- **os.environ,
- "GIT_CONFIG_NOSYSTEM": "1",
- "GIT_CONFIG_GLOBAL": os.devnull,
- "GIT_NO_REPLACE_OBJECTS": "1",
- "LC_ALL": "C",
- "TZ": "UTC",
- },
+ env=git_env,
timeout=GIT_TIMEOUT_SECONDS,
check=False,
)
@@ -844,9 +889,15 @@ def _build_discovery(epoch_n: int, boundary_ms: int, events: list) -> GitDiscove
canonical_location = min(
locations[qualified_oid], key=lambda x: (x[0], x[1])
)
+ # One commit may be reachable from dozens of refs in the same mirror.
+ # Verify its object once per repository, not once per source ref.
+ object_locations = {
+ (str(m), raw_oid): (m, raw_oid)
+ for _, _, m, raw_oid in locations[qualified_oid]
+ }
object_hashes = {
hashlib.sha256(_git(m, "cat-file", "commit", raw_oid)).hexdigest()
- for _, _, m, raw_oid in locations[qualified_oid]
+ for m, raw_oid in object_locations.values()
}
if len(object_hashes) != 1:
raise RuntimeError(f"conflicting Git objects share OID {qualified_oid}")
@@ -994,11 +1045,55 @@ async def discover_repositories(epoch_n: int, boundary_ms: int) -> GitDiscovery:
SSE_CLIENTS = []
+AUDIT_HISTORY = []
+AUDIT_SEQUENCE = 0
+PROCESS_STATE = {
+ "running": False,
+ "phase": "idle",
+ "progress": 100,
+ "message": "Waiting for the next epoch",
+}
+
+
+def _sse_event(event_name: str, payload: dict) -> str:
+ return (
+ f"event: {event_name}\n"
+ f"data: {json.dumps(payload, separators=(',', ':'))}\n\n"
+ )
+
+
+async def broadcast_audit(
+ kind: str,
+ message: str,
+ *,
+ progress: int | None = None,
+ phase: str | None = None,
+) -> dict:
+ global AUDIT_SEQUENCE
+ AUDIT_SEQUENCE += 1
+ if progress is not None:
+ PROCESS_STATE["progress"] = max(0, min(100, int(progress)))
+ if phase is not None:
+ PROCESS_STATE["phase"] = phase
+ PROCESS_STATE["message"] = message
+ payload = {
+ "id": AUDIT_SEQUENCE,
+ "timestamp_ms": int(time.time() * 1000),
+ "kind": kind,
+ "message": message,
+ **PROCESS_STATE,
+ }
+ AUDIT_HISTORY.append(payload)
+ del AUDIT_HISTORY[:-200]
+ wire = _sse_event("audit", payload)
+ for queue in list(SSE_CLIENTS):
+ await queue.put(wire)
+ return payload
async def broadcast_js(js: str):
"""Send a JS snippet to all connected SSE clients."""
- for queue in SSE_CLIENTS:
+ for queue in list(SSE_CLIENTS):
await queue.put(js)
@@ -1006,10 +1101,29 @@ async def rank_commits(commits: list[dict]):
if not commits:
return {}, []
- models = await fetch_top_models(n=3)
contributors = sorted(set(c["contributor"] for c in commits))
- if len(contributors) > 1 and not models:
+ if len(contributors) == 1:
+ await broadcast_audit(
+ "ranking",
+ f"Only {contributors[0]} is eligible; rank is 1.0",
+ progress=90,
+ phase="finalizing",
+ )
+ return {contributors[0]: Decimal("1")}, []
+ 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)
+ if not models:
raise RuntimeError("no council models available for contributor ranking")
+ await broadcast_audit(
+ "council",
+ f"Council selected: {', '.join(models)}",
+ progress=35,
+ phase="ranking",
+ )
await broadcast_js(exec_event(Three[Selector("#emission-log")][PREPEND][
["div.log-council", f"Council: {', '.join(models)} — {len(commits)} commits"]
]))
@@ -1035,6 +1149,11 @@ async def rank_commits(commits: list[dict]):
async def compare_fn(i, j):
a1, a2 = authors[i], authors[j]
+ await broadcast_audit(
+ "comparison",
+ f"Comparing {a1} with {a2}",
+ phase="ranking",
+ )
await broadcast_js(exec_event(Three[Selector("#emission-status")][MORPH][
["div#emission-status", f"Comparing {a1} vs {a2}…"]
]))
@@ -1050,6 +1169,11 @@ async def rank_commits(commits: list[dict]):
if winner_weight <= 0 or loser_weight <= 0:
raise ValueError("ratio weights must be positive")
results.append((w, l, winner_weight, loser_weight))
+ await broadcast_audit(
+ "vote",
+ f"{model}: {authors[w]} over {authors[l]} ({result['ratio']})",
+ phase="ranking",
+ )
await broadcast_js(exec_event(Three[Selector("#emission-log")][PREPEND][
["div.log-vote",
["span.model", model], " — ",
@@ -1059,6 +1183,11 @@ async def rank_commits(commits: list[dict]):
]
]))
except Exception as e:
+ await broadcast_audit(
+ "error",
+ f"{model} failed: {e}",
+ phase="error",
+ )
await broadcast_js(exec_event(Three[Selector("#emission-log")][PREPEND][
["div.log-error", f"⚠ {model}: {e}"]
]))
@@ -1068,8 +1197,13 @@ async def rank_commits(commits: list[dict]):
async def progress_fn(ev):
if ev["phase"] == "spanning_tree":
label = f"Spanning tree: {ev['step']}/{ev['total']}"
+
… preview truncated; 28,089 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.