Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-core/src/race-judge.ts

Snapshot at 34e3175

Containing directory

2,258 bytes · typescriptView raw
1import { HttpError } from "./http";
2import { entrantKey, entrants } from "./race-entrants";
3import type { Packet, RaceEntrant, RaceRecord, RaceVerdict, Review } from "./workflow-types";
4
5type Storage = DurableObjectStorage | DurableObjectTransaction;
6
7/** A verdict-only record does not finish a race or block an owner promotion. */
8export async function raceFinished(storage: Storage, packet: string): Promise<boolean> {
9  return Boolean((await storage.get<RaceRecord>(`race:${packet}`))?.finishedAt);
10}
11
12/** The sole cross-agent review exception: immutable judge and exact entrant binding. */
13export async function judgeMayRead(storage: Storage, agent: string | undefined, review: Review): Promise<boolean> {
14  if (!agent) return false;
15  const packet = await storage.get<Packet>(`packet:${review.packet}`);
16  if (packet?.mode !== "race" || packet.judge !== agent) return false;
17  const entrant = await storage.get<RaceEntrant>(entrantKey(review.packet, review.agent));
18  return entrant?.packet === review.packet && entrant.agent === review.agent && entrant.repoName === review.repoName;
19}
20
21/** Invoked under the Supervisor gate; persist the roster check and advice together. */
22export async function saveVerdict(storage: DurableObjectStorage, packet: string, data: Record<string, unknown>): Promise<RaceVerdict> {
23  return storage.transaction(async txn => {
24    const key = `race:${packet}`;
25    const current = await txn.get<RaceRecord>(key);
26    if (current?.finishedAt) throw new HttpError(409, "Race finished.");
27    const roster = await entrants(txn, packet);
28    const { ranking, rationale } = data;
29    if (!Array.isArray(ranking) || ranking.length !== roster.length ||
30        !ranking.every(agent => typeof agent === "string" && roster.some(entrant => entrant.agent === agent)) ||
31        new Set(ranking).size !== ranking.length ||
32        typeof rationale !== "string" || rationale.trim().length === 0 || rationale.length > 2000)
33      throw new HttpError(400, "ranking must be a permutation of the current entrants; rationale must be 1–2000 characters.");
34    const verdict: RaceVerdict = { ranking, rationale, at: new Date().toISOString() };
35    await txn.put(key, { ...current, verdict } satisfies RaceRecord);
36    return verdict;
37  });
38}
39