Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/src/judge.ts
Snapshot at 34e3175
1import type { ProjectState, Race, RaceEntrant, Review } from "./contracts.ts";
2import { promotionAvailable, reviewGate, reviewState } from "./model.ts";
3import { packetPushes } from "./fork-graph-model.ts";
4import { ordinal } from "./race-model.ts";
5
6export const rankingWord = (index: number) => index === 0 ? "Pick" : `${ordinal(index + 1)} choice`;
7export function entrantReview(state: ProjectState, race: Race, agent: string): Review | undefined {
8 const entrant = race.entrants.find((e) => e.agent === agent);
9 return state.reviews.filter((r) => r.packet === race.packet && r.agent === agent &&
10 r.repoName === entrant?.repoName).toSorted((a, b) =>
11 (b.requestedAt ?? "").localeCompare(a.requestedAt ?? "") || b.id.localeCompare(a.id))[0];
12}
13/** Ranking is of agents. Later review or push evidence must never authorize unseen work. */
14export function pushedAfterAdvice(state: ProjectState, race: Race, entrant: RaceEntrant): boolean {
15 const at = Date.parse(race.verdict?.at ?? "");
16 if (!Number.isFinite(at)) return true;
17 const review = entrantReview(state, race, entrant.agent);
18 return Date.parse(review?.requestedAt ?? "") > at ||
19 Date.parse(entrant.firstPushAt ?? "") > at ||
20 packetPushes(state.recentActivity ?? [], entrant.repoName, state.project.defaultBranch,
21 Math.max(at + 1, Date.parse(entrant.claimedAt)), Date.now()).length > 0;
22}
23export function judgePromotion(state: ProjectState, race: Race) {
24 const pick = race.verdict?.ranking[0];
25 const entrant = race.entrants.find((e) => e.agent === pick);
26 const review = pick ? entrantReview(state, race, pick) : undefined;
27 let reason = "";
28 if (!race.judge || !pick || race.status === "finished") reason = "";
29 else if (!entrant) reason = `${pick} is no longer in this race.`;
30 else if (pushedAfterAdvice(state, race, entrant)) reason = `${pick} pushed after this advice.`;
31 else if (entrant.closedAt) reason = `${pick} left the race.`;
32 else if (!review) reason = `${pick} hasn't requested review yet.`;
33 else if (state.promotionPending) reason = "Promotion outcome pending owner inspection.";
34 else {
35 const cause = reviewState(review, state);
36 const gate = reviewGate(review, state);
37 if (cause.kind !== "current") reason = cause.label;
38 else if (!gate.approve && !gate.promote) reason = gate.reason;
39 else if (review.status !== "approved") reason = `${pick}'s review isn't approved yet.`;
40 else if (!promotionAvailable(review, state)) reason = "Re-checking…";
41 }
42 return { pick, review, reason, available: !!race.judge && !!pick && !!review && !!entrant &&
43 race.status === "open" && !entrant.closedAt && !reason && promotionAvailable(review, state) };
44}
45export function adviceForReview(state: ProjectState, review: Review) {
46 const race = state.races?.find((r) => r.packet === review.packet && r.judge && r.verdict);
47 if (!race?.verdict || !race.entrants.some((e) => e.agent === review.agent && e.repoName === review.repoName)) return;
48 const index = race.verdict.ranking.indexOf(review.agent);
49 if (index < 0) return;
50 return { race, index, at: race.verdict.at };
51}
52