import type { ApiResponse, ErrorCode, EvalLaneSummary, EvalRunRequest, EvalRunResult, ArtifactRef, ArtifactDetail, ReplayComparison, ProposalDetail, ProposalState, ProposalSummary, MathProposalSummary, MathProposalDetail, MathRatifyResult, } from "../types/api"; export class WorkbenchApiError extends Error { constructor( public readonly code: ErrorCode, message: string, ) { super(message); this.name = "WorkbenchApiError"; } } export const API_URL: string = import.meta.env.VITE_WORKBENCH_API_URL ?? "http://127.0.0.1:8765"; export async function apiFetch(path: string, init?: RequestInit): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const res = await fetch(`${API_URL}${path}`, { ...init, signal: controller.signal }); const json: ApiResponse = await res.json(); if (!json.ok) { throw new WorkbenchApiError(json.error.code, json.error.message); } return json.data; } finally { clearTimeout(timeout); } } export async function fetchEvalLanes(): Promise { return apiFetch("/evals"); } export async function runEvalLane(req: EvalRunRequest): Promise { if (req.split === "holdout") { const hasConfig = typeof window !== "undefined" && (window as any).sealedEvalConfig === true; if (!hasConfig) { throw new WorkbenchApiError( "client_refused_sealed_holdout", "Holdout runs require sealed-eval config — use CLI" ); } } const lanes = await fetchEvalLanes(); const lane = lanes.find((l) => l.lane === req.lane); if (!lane) { throw new WorkbenchApiError("not_found", `Eval lane not found: ${req.lane}`); } if (!lane.read_only) { throw new WorkbenchApiError("client_refused_unsafe_lane", "API run disabled — use CLI"); } return apiFetch("/evals/run", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); } export async function fetchArtifacts(): Promise { const data = await apiFetch<{ items: ArtifactRef[] }>("/artifacts"); return data.items; } export async function fetchArtifactDetail(artifactId: string): Promise { return apiFetch(`/artifacts/${artifactId}`); } export async function fetchReplayComparison(artifactId: string): Promise { return apiFetch(`/replay/${artifactId}`); } export type ProposalStateFilter = ProposalState | "all"; interface ItemsEnvelope { items: T[]; } export async function fetchProposals( filter: ProposalStateFilter = "all", ): Promise { const envelope = await apiFetch>("/proposals"); if (filter === "all") { return envelope.items; } return envelope.items.filter((proposal) => proposal.state === filter); } export async function fetchProposalDetail(proposalId: string): Promise { return apiFetch(`/proposals/${encodeURIComponent(proposalId)}`); } export async function fetchMathProposals(): Promise { const envelope = await apiFetch>("/math-proposals"); return envelope.items; } export async function fetchMathProposalDetail(proposalId: string): Promise { return apiFetch(`/math-proposals/${encodeURIComponent(proposalId)}`); } export async function ratifyMathProposal( proposalId: string, category?: string, polarity?: string, dryRun?: boolean, ): Promise { return apiFetch(`/math-proposals/${encodeURIComponent(proposalId)}/ratify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category, polarity, dry_run: dryRun }), }); } export async function rejectMathProposal( proposalId: string, note?: string, ): Promise<{ proposal_id: string; rejected: boolean }> { return apiFetch<{ proposal_id: string; rejected: boolean }>( `/math-proposals/${encodeURIComponent(proposalId)}/reject`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ note }), }, ); } export async function deferMathProposal( proposalId: string, ): Promise<{ proposal_id: string; deferred: boolean }> { return apiFetch<{ proposal_id: string; deferred: boolean }>( `/math-proposals/${encodeURIComponent(proposalId)}/defer`, { method: "POST", }, ); }