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

packages/forkhorse-dashboard/src/judge.ts

Snapshot at 34e3175

Containing directory

3,106 bytes · typescriptView raw
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