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

packages/forkhorse-gatekeeper/src/worker/workflow-evidence.ts

Snapshot at 34e3175

Containing directory

3,248 bytes · typescriptView raw
1import type { WorkflowContext, WorkflowWrite } from "./workflow.ts";
2
3/** Public identifiers captured before sending; existing forks cannot prove a new join. */
4export interface Evidence {
5  agent: string;
6  project: string;
7  forks: string[];
8  claim: string | null;
9}
10const claimKey = (claim: { repoName: string; createdAt?: string; claimedAt?: string }) =>
11  JSON.stringify([claim.repoName, claim.createdAt ?? claim.claimedAt]);
12export function evidence(context: WorkflowContext, agent: string, operation: WorkflowWrite): Evidence {
13  if (!Array.isArray(context.forks) || !Array.isArray(context.claims) || !Array.isArray(context.races) || !Array.isArray(context.reviews)) {
14    throw new Error("Workflow context is incomplete. Check the project's forks, claims, races and reviews.");
15  }
16  const packet = "packet" in operation ? operation.packet : undefined;
17  const claim = context.claims.find(item => item.packet === packet && item.agent === agent);
18  const entrant = context.races.find(race => race.packet === packet)?.entrants.find(item => item.agent === agent && !item.closedAt);
19  return { agent, project: context.project.id, forks: context.forks.filter(fork => fork.agent === agent).map(fork => fork.repoName), claim: claim ? claimKey(claim) : entrant ? claimKey(entrant) : null };
20}
21export function outcome(context: WorkflowContext, before: Evidence, operation: WorkflowWrite, race: boolean): "applied" | "absent" | "ambiguous" {
22  const now = evidence(context, before.agent, operation);
23  if (now.project !== before.project) return "ambiguous";
24  switch (operation.kind) {
25    case "join": {
26      const added = now.forks.filter(name => !before.forks.includes(name));
27      if (added.length === 1) return "applied";
28      return added.length === 0 && before.forks.every(name => now.forks.includes(name)) ? "absent" : "ambiguous";
29    }
30    case "claim": {
31      if (race) {
32        const entrants = context.races.find(item => item.packet === operation.packet)?.entrants;
33        if (!entrants) return "ambiguous";
34        const entrant = entrants.find(item => item.agent === before.agent);
35        if (!entrant) return "absent";
36        return entrant.repoName === operation.repoName ? "applied" : "ambiguous";
37      }
38      const claim = context.claims.find(item => item.packet === operation.packet && item.agent === before.agent);
39      if (!claim) return "absent";
40      return claim.repoName === operation.repoName ? "applied" : "ambiguous";
41    }
42    case "requestReview":
43      return context.reviews.some(review => review.agent === before.agent && review.packet === operation.packet && review.repoName === operation.repoName && review.sha === operation.sha) ? "applied" : "absent";
44    case "release":
45      if (now.claim === null) return "applied";
46      return now.claim === before.claim ? "absent" : "ambiguous";
47  }
48}
49export function uncertain(operation: WorkflowWrite): Error {
50  const check = operation.kind === "join" ? "the connected agent's new forks" : operation.kind === "requestReview" ? `reviews for commit ${operation.sha.slice(0, 7)}` : `claims and race entrants for ${operation.packet}`;
51  return new Error(`Workflow outcome uncertain. Check ${check} in context; this call will not blindly resend the write.`);
52}
53