Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/src/worker/workflow-evidence.ts
Snapshot at 34e3175
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