// Authenticated client for the Matrix Builder /v1 workflow + generation API. // // Every call carries the self-issued session JWT (Authorization: Bearer …) via authHeaders(), // so each UI action maps to exactly one typed, owner-scoped request. This is the single client // the build screens should reach for; api-client.ts (unauthenticated) is being retired. import { apiBaseUrl } from "./api-client"; import { authHeaders } from "./auth-token"; import type { BlueprintCandidateContract, BlueprintResultContract, ContractCoderId, IdeaRequestContract, MatrixBundleContract, PromptResponseContract, ValidationReportContract, } from "../types/contracts"; import type { ArtifactResponse, BatchResponse, BlueprintImportResponse, ChangeType, CommitDiffResponse, CommitResponse, ExecutionRequest, ExecutionResponse, IdeaIntentResponse, IngestDocumentResponse, ProjectCreate, ProjectResponse, PromptPackResponse, RunEnqueueResponse, RunEvent, RunResponse, TimelineResponse, ValidationRunResponse, VersionCreate, VersionResponse, } from "./workflow-types"; export class WorkflowApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = "WorkflowApiError"; } } async function request(path: string, init?: RequestInit): Promise { const response = await fetch(`${apiBaseUrl}${path}`, { ...init, headers: { "content-type": "application/json", ...authHeaders(), ...(init?.headers ?? {}), }, }); if (!response.ok) { let detail = `Workflow API error ${response.status}`; try { const body = (await response.json()) as { detail?: string }; if (body?.detail) detail = body.detail; } catch { // non-JSON error body; keep the default message } throw new WorkflowApiError(response.status, detail); } // 204/empty bodies are valid for some endpoints; tolerate them. if (response.status === 204) return undefined as T; const text = await response.text(); return (text ? JSON.parse(text) : undefined) as T; } function post(path: string, body?: unknown): Promise { return request(path, { method: "POST", body: body === undefined ? undefined : JSON.stringify(body) }); } // --- Projects ----------------------------------------------------------------------------- export function createProject(payload: ProjectCreate): Promise { return post(`/api/v1/projects`, payload); } export function listProjects(): Promise { return request(`/api/v1/projects`); } export function getProject(projectId: string): Promise { return request(`/api/v1/projects/${projectId}`); } // --- Versions ----------------------------------------------------------------------------- export function createVersion(payload: VersionCreate): Promise { return post(`/api/v1/versions`, payload); } export function getVersion(versionId: string): Promise { return request(`/api/v1/versions/${versionId}`); } export function getTimeline(versionId: string): Promise { return request(`/api/v1/versions/${versionId}/timeline`); } // --- Batches ------------------------------------------------------------------------------ export function createBatch( versionId: string, goalMd: string, changeType: ChangeType, title?: string, ): Promise { return post(`/api/v1/batches`, { version_id: versionId, goal_md: goalMd, change_type: changeType, ...(title ? { title } : {}), }); } export function getBatch(batchId: string): Promise { return request(`/api/v1/batches/${batchId}`); } export function generatePromptPack(batchId: string, coder: string): Promise { return post(`/api/v1/batches/${batchId}/prompt-pack`, { coder }); } // --- Execution: submit what the coder changed (commit + validate in one call) -------------- export function submitExecution( batchId: string, payload: ExecutionRequest, ): Promise { return post(`/api/v1/batches/${batchId}/executions`, payload); } // --- Async runs (enqueue, then poll/stream events) ----------------------------------------- export function enqueueRun(batchId: string, payload: ExecutionRequest): Promise { return post(`/api/v1/batches/${batchId}/runs`, payload); } export function getRun(runId: string): Promise { return request(`/api/v1/runs/${runId}`); } export function getRunEvents(runId: string, after = 0): Promise { return request(`/api/v1/runs/${runId}/events?after=${after}`); } export function getValidationRun(runId: string): Promise { return request(`/api/v1/validation-runs/${runId}`); } export function createRepairBatch(runId: string, coder: string): Promise { return post(`/api/v1/repair-batches`, { validation_run_id: runId, coder }); } // --- Commits, diffs and artifacts (the immutable record of an accepted change) ------------- export function getCommit(commitId: string): Promise { return request(`/api/v1/commits/${commitId}`); } export function getCommitDiff(commitId: string): Promise { return request(`/api/v1/commits/${commitId}/diff`); } export function getCommitArtifacts(commitId: string): Promise { return request(`/api/v1/commits/${commitId}/artifacts`); } // --- Generation (idea → candidates → bundle), now auth-headered too ------------------------ export function parseIdea(payload: Partial): Promise { return post(`/api/v1/ideas/parse`, payload); } export function getBlueprintCandidates( payload: Partial, ): Promise<{ candidates: BlueprintCandidateContract[] }> { return post(`/api/v1/blueprints/candidates`, payload); } export function generateBlueprint( payload: Partial, candidateId?: string, ): Promise { return post(`/api/v1/blueprints/generate`, { idea_request: payload, ...(candidateId ? { candidate_id: candidateId } : {}), }); } export function generateBundle( ideaRequest: Partial, preferredCoder: ContractCoderId, candidateId?: string, ): Promise { return post(`/api/v1/bundles`, { idea_request: ideaRequest, preferred_coder: preferredCoder, ...(candidateId ? { candidate_id: candidateId } : {}), }); } // --- Import existing plan: brief upload (Path B) and Blueprint JSON (Path C, skip-AI) ---------- // Upload a PDF/DOCX/Markdown/TXT brief; returns a deterministic ProjectBrief + a derived idea. export async function ingestDocument(file: File): Promise { const form = new FormData(); form.append("file", file); // No content-type header: the browser sets the multipart boundary. const response = await fetch(`${apiBaseUrl}/api/v1/ingest/document`, { method: "POST", headers: { ...authHeaders() }, body: form, }); if (!response.ok) { let detail = `Upload failed (${response.status})`; try { const b = (await response.json()) as { detail?: string }; if (b?.detail) detail = b.detail; } catch { /* keep default */ } throw new WorkflowApiError(response.status, detail); } return (await response.json()) as IngestDocumentResponse; } // Validate a complete Blueprint JSON against the contract (no AI). valid=false carries errors. export function importBlueprint(blueprint: unknown): Promise { return post(`/api/v1/ingest/blueprint`, { blueprint }); } // Compile a validated blueprint verbatim into a Matrix Bundle — AI skipped. export function generateBundleFromBlueprint( blueprint: unknown, preferredCoder: ContractCoderId, ): Promise { return post(`/api/v1/bundles`, { blueprint, preferred_coder: preferredCoder }); } export function getBundle(bundleId: string): Promise { return request(`/api/v1/bundles/${bundleId}`); } export function getBundlePrompt( bundleId: string, coder: ContractCoderId, ): Promise { return request(`/api/v1/bundles/${bundleId}/prompt/${coder}`); } export function validateBundle(bundleId: string): Promise { return post(`/api/v1/bundles/${bundleId}/validate`, {}); } // Validate a submitted patch (the files the AI coder changed) against the bundle's contract. // Stateless and real (the metadata contract check) — returns findings/score without a DB-backed run. export function validateChanges( bundleId: string, changedFiles: Array<{ path: string; status?: "added" | "modified" | "deleted" | "renamed" }>, ): Promise { return post(`/api/v1/validation/patch`, { bundle_id: bundleId, mode: "patch", changed_files: changedFiles.map((f) => ({ path: f.path, status: f.status ?? "modified" })), }); } // The engine's bundle zip (byte-for-byte what the CLI ships). Fetched with auth so owner-scoped // bundles download; returns the raw Blob for the browser to save. export async function downloadBundleZip(bundleId: string): Promise { const response = await fetch(`${apiBaseUrl}/api/v1/bundles/${bundleId}/download`, { headers: { ...authHeaders() }, }); if (!response.ok) throw new WorkflowApiError(response.status, "bundle download unavailable"); return response.blob(); } // Thumbnails are owner-scoped; fetch with auth and inline the SVG markup. export async function fetchThumbnail(versionId: string): Promise { const response = await fetch(`${apiBaseUrl}/api/v1/versions/${versionId}/thumbnail.svg`, { headers: { ...authHeaders() }, }); if (!response.ok) throw new WorkflowApiError(response.status, "thumbnail unavailable"); return response.text(); }