Side A is a substantive, well-tested refactor of URL canonicalization into a composable rule engine (url_rules module) that fixes real correctness issues (scheme-qualified ItemId, reddit/youtube normalization, parent/breadcrumb logic) with extensive new unit tests. Side B is largely infra/ops scaffolding (Dockerfile, fly.toml, CI) plus a dashboard UI and audit-broadcast plumbing that adds operational value but is less architecturally durable and more application-specific/generated-feeling (large CSS/JS blob, repo-list config tweaks).
constitution · epochs · watch · epoch 3
c_77729db919ab (tommy-mor) vs c_7a129e904906 (tommy-mor)
download prompt · raw event · cmp_f75974e4c5fc53
council reasoning
A delivers lasting core design: a composable url_rules engine, scheme-full canonical ItemIds, and correct parent/breadcrumb hierarchy (e.g. skipping phantom /comments nodes) wired through identity, Reddit mapping, and projection apply. B’s deploy path, status API, epoch/OpenRouter guards, and multi-repo config are real operational value, but a large share is dashboard CSS/JS surface and infra wiring rather than durable domain structure.
Side A introduces a substantive URL canonicalization architecture by extracting normalization into composable `url_rules` modules, making `ItemId` consistently use canonical HTTPS URLs, fixing parent/breadcrumb handling, and updating projection/event parsing to canonicalize stored IDs. Side B adds valuable deployment infrastructure, a live audit dashboard, SSE progress reporting, and CI/CD, but those are primarily operational features rather than core data-model improvements; the canonical identity changes in A have broader long-term impact on correctness and consistency.
sides
A — c_77729db919ab (tommy-mor)
message
[239c074b] url schema stuff
diff preview
diff --git a/AGENTS.md b/AGENTS.md
index 426a88e7c1da54fe0a28c5c76fa4e1f1bc117fcf..e60b9ba6012593361ef10e8fdd9439cd9932e09b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -58,3 +58,4 @@ Use **tmux** for `cargo run --package sorter2-server` (dev server). Rebuild afte
- First `cargo test` / `cargo build --release` is slow; Clojure smoke test always does a release build.
- `legacy/` and `ideas/` are not part of the workspace build.
+- **ItemId** for web URLs is a canonical full URL (`https://reddit.com/r/rust`). Rules live in [`server/src/url_rules/`](server/src/url_rules/) (composable Rust, not a config DSL). After changing canonicalization rules, rebuild the projection: `cargo run --package sorter2-server -- replay-index`.
diff --git a/Cargo.lock b/Cargo.lock
index 0dd4fce5fb6400ae153cca4e3dbf5a5158e6d8b4..49a908ef935c430dbe63c6a28d8a24e38b489486 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1951,6 +1951,7 @@ dependencies = [
"tower-http 0.5.2",
"tracing",
"tracing-subscriber",
+ "url",
"urlencoding",
]
diff --git a/REPLAY.sh b/REPLAY.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f2dbd8aea60c02d2feef74805f7ef5c2b7022537
--- /dev/null
+++ b/REPLAY.sh
@@ -0,0 +1,2 @@
+cargo run --package sorter2-server -- replay-index
+
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 27f552c20b97ef28cdde4cb6b1a4980375135111..ad4912791aff59fb1d3293f66ad381ae618cd60b 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -24,6 +24,7 @@ async-stream = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
rand = "0.8"
urlencoding = "2"
+url = "2"
durable = { path = "../durable" }
[dev-dependencies]
diff --git a/server/src/entity_store.rs b/server/src/entity_store.rs
index d5d17c3676e4a8ddec998e9f5a9dbafe9c2d9d0e..d29f39aecca6f12cdcf263cf77c3654eb4ee6cfa 100644
--- a/server/src/entity_store.rs
+++ b/server/src/entity_store.rs
@@ -124,7 +124,7 @@ mod tests {
fn round_trip_payload() {
let tmp = tempfile::tempdir().unwrap();
let store = EntityStore::open(tmp.path()).unwrap();
- let id = ItemId::parse("reddit.com/r/rust").unwrap();
+ let id = ItemId::from_url("https://reddit.com/r/rust").unwrap();
let payload = json!({"kind": "t5", "data": {"display_name": "rust"}});
store.put(&id, &payload).unwrap();
diff --git a/server/src/event_log.rs b/server/src/event_log.rs
index 36f5b406084065b608735987cdb483c236e03081..2c9290b6fdbf2c2ad1c0f1ffd7374b2d9cc97f36 100644
--- a/server/src/event_log.rs
+++ b/server/src/event_log.rs
@@ -199,7 +199,7 @@ mod tests {
log.append(&sample_record(
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
))
.await
@@ -237,7 +237,7 @@ mod tests {
let path = tmp.path().join("events.jsonl");
let log = EventLog::new(&path);
let event = Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
};
log.append(&sample_record(1, event)).await.unwrap();
@@ -255,7 +255,7 @@ mod tests {
let path = tmp.path().join("events.jsonl");
std::fs::write(
&path,
- r#"{"type":"node_ensured","id":"reddit.com/r/rust"}
+ r#"{"type":"node_ensured","id":"https://reddit.com/r/rust"}
{"schema":1,"seq":1,"ts":1,"event":{"type":"vote_recorded","ts":1,"a":"a","b":"b","ratio_left":2,"ratio_right":1,"scope":""}}
"#,
)
@@ -295,7 +295,7 @@ mod tests {
log.append(&sample_record(
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
))
.await
@@ -303,7 +303,7 @@ mod tests {
log.append(&sample_record(
3,
Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
},
))
.await
diff --git a/server/src/journal.rs b/server/src/journal.rs
index 521a108019de1ea870d14c4fafbfe572c20ce0de..50bc89f976edb82b7b0e49e954a8eccbbe82bf87 100644
--- a/server/src/journal.rs
+++ b/server/src/journal.rs
@@ -141,10 +141,10 @@ mod tests {
let j2 = journal.clone();
let (r1, r2) = tokio::join!(
j1.append(Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
}),
j2.append(Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
}),
);
r1.unwrap();
@@ -153,10 +153,10 @@ mod tests {
assert_eq!(projection_store.last_applied_event_count().unwrap(), 2);
let tree = projection_store.load_tree().unwrap();
assert!(tree
- .get(&ItemId::parse("reddit.com/r/rust").unwrap())
+ .get(&ItemId::parse("https://reddit.com/r/rust").unwrap())
.is_some());
assert!(tree
- .get(&ItemId::parse("reddit.com/r/python").unwrap())
+ .get(&ItemId::parse("https://reddit.com/r/python").unwrap())
.is_some());
}
@@ -170,7 +170,7 @@ mod tests {
1,
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
))
.await
@@ -186,7 +186,7 @@ mod tests {
1,
1,
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
)],
)
@@ -202,7 +202,7 @@ mod tests {
);
journal
.append(Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
})
.await
.unwrap();
@@ -227,13 +227,13 @@ mod tests {
journal
.append_many(vec![
Event::NodeEnsured {
- id: "reddit.com/r/rust".into(),
+ id: "https://reddit.com/r/rust".into(),
},
Event::NodeEnsured {
- id: "reddit.com/r/python".into(),
+ id: "https://reddit.com/r/python".into(),
},
Event::NodeEnsured {
- id: "reddit.com/r/clojure".into(),
+ id: "https://reddit.com/r/clojure".into(),
},
])
.await
@@ -245,7 +245,7 @@ mod tests {
assert_eq!(projection_store.last_applied_event_count().unwrap(), 3);
let tree = projection_store.load_tree().unwrap();
assert!(tree
- .get(&ItemId::parse("reddit.com/r/clojure").unwrap())
+ .get(&ItemId::parse("https://reddit.com/r/clojure").unwrap())
.is_some());
}
}
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 9bd5f76fd1406b9b1be4c272f4ba8647edde2678..5c02c8e704e4664453bad75d819df8a067668176 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -9,6 +9,7 @@ pub mod journal;
pub mod pair;
pub mod parser;
pub mod path_types;
+pub mod url_rules;
pub mod projection_apply;
pub mod projection_store;
pub mod ranking;
diff --git a/server/src/pair.rs b/server/src/pair.rs
index 43f780ba6ea6ce1cdc2e1f4cbb252ba8a10684b9..815a97b80e3e9f348e0937a4f147f2862018edb0 100644
--- a/server/src/pair.rs
+++ b/server/src/pair.rs
@@ -381,42 +381,42 @@ mod tests {
#[test]
fn suggest_prefers_unvoted_pair() {
- let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+ let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
let mut tree = seed_children(
&parent,
&[
- "reddit.com/r/rust/a",
- "reddit.com/r/rust/b",
- "reddit.com/r/rust/c",
+ "https://reddit.com/r/rust/a",
+ "https://reddit.com/r/rust/b",
+ "https://reddit.com/r/rust/c",
],
);
let vote =
- VoteData::from_recorded(1, "reddit.com/r/rust/a", "reddit.com/r/rust/b", 2, 1).unwrap();
+ VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
tree.apply_vote(&parent, vote);
let group = tree.get(&parent).unwrap().local_ranking.clone();
let pool = children_of(&tree, &parent);
let (l, r) = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
- let voted_ab = (l.as_str() == "reddit.com/r/rust/a" && r.as_str() == "reddit.com/r/rust/b")
- || (l.as_str() == "reddit.com/r/rust/b" && r.as_str() == "reddit.com/r/rust/a");
+ let voted_ab = (l.as_str() == "https://reddit.com/r/rust/a" && r.as_str() == "https://reddit.com/r/rust/b")
+ || (l.as_str() == "https://reddit.com/r/rust/b" && r.as_str() == "https://reddit.com/r/rust/a");
assert!(!voted_ab);
}
#[test]
fn suggest_bridges_separate_components() {
- let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+ let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
let mut tree = seed_children(
&parent,
&[
- "reddit.com/r/rust/a",
- "reddit.com/r/rust/b",
- "reddit.com/r/rust/c",
- "reddit.com/r/rust/d",
+ "https://reddit.com/r/rust/a",
+ "https://reddit.com/r/rust/b",
+ "https://reddit.com/r/rust/c",
+ "https://reddit.com/r/rust/d",
],
);
let ab =
- VoteData::from_recorded(1, "reddit.com/r/rust/a", "reddit.com/r/rust/b", 2, 1).unwrap();
+ VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
let cd =
- VoteData::from_recorded(2, "reddit.com/r/rust/c", "reddit.com/r/rust/d", 2, 1).unwrap();
+ VoteData::from_recorded(2, "https://reddit.com/r/rust/c", "https://reddit.com/r/rust/d", 2, 1).unwrap();
tree.apply_vote(&parent, ab);
tree.apply_vote(&parent, cd);
let group = tree.get(&parent).unwrap().local_ranking.clone();
@@ -424,37 +424,37 @@ mod tests {
let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
let chosen = pair_set(&pair);
let from_ab =
- chosen.contains("reddit.com/r/rust/a") || chosen.contains("reddit.com/r/rust/b");
+ chosen.contains("https://reddit.com/r/rust/a") || chosen.contains("https://reddit.com/r/rust/b");
let from_cd =
- chosen.contains("reddit.com/r/rust/c") || chosen.contains("reddit.com/r/rust/d");
+ chosen.contains("https://reddit.com/r/rust/c") || chosen.contains("https://reddit.com/r/rust/d");
assert!(from_ab && from_cd, "expected bridge pair, got {:?}", chosen);
}
#[test]
fn suggest_prefers_attach_over_isolate_pair_among_many_unranked() {
- let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+ let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
let mut tree = seed_children(
&parent,
&[
- "reddit.com/r/rust/a",
- "reddit.com/r/rust/b",
- "reddit.com/r/rust/c",
- "reddit.com/r/rust/d",
- "reddit.com/r/rust/e",
+ "https://reddit.com/r/rust/a",
+ "https://reddit.com/r/rust/b",
+ "https://reddit.com/r/rust/c",
+ "https://reddit.com/r/
… preview truncated; 51,799 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.