import { env } from "@code/env/convex"; import { canTransitionWorkDelivery, decodeWorkArtifactDraft, decodeWorkDeliveryDraft, } from "@code/primitives/work-artifact"; import { ConvexError, v } from "convex/values"; import { Effect } from "effect"; import type { Id } from "./_generated/dataModel"; import { mutation, query } from "./_generated/server"; import type { MutationCtx } from "./_generated/server"; import { requireProjectMember } from "./authz"; const artifactKind = v.union( v.literal("definition"), v.literal("design"), v.literal("diff"), v.literal("test-report"), v.literal("verification-report"), v.literal("runtime-log"), v.literal("screenshot"), v.literal("video"), v.literal("commit"), v.literal("branch"), v.literal("pull-request"), v.literal("preview"), v.literal("deployment"), v.literal("other") ); const verificationStatus = v.union( v.literal("unverified"), v.literal("verified"), v.literal("rejected") ); const deliveryKind = v.union( v.literal("branch"), v.literal("commit"), v.literal("pull-request"), v.literal("preview"), v.literal("deployment") ); const deliveryStatus = v.union( v.literal("recorded"), v.literal("ready"), v.literal("approved"), v.literal("delivered"), v.literal("failed"), v.literal("cancelled") ); const requireAgent = (token: string): void => { if (token !== env.FLUE_DB_TOKEN) { throw new ConvexError("Invalid agent control token"); } }; const appendEvent = async ( ctx: MutationCtx, workId: Id<"works">, kind: "artifact.recorded" | "delivery.recorded" | "delivery.updated", idempotencyKey: string, referenceId: string, payloadJson: string ): Promise => { const existing = await ctx.db .query("workEvents") .withIndex("by_work_and_idempotencyKey", (q) => q.eq("workId", workId).eq("idempotencyKey", idempotencyKey) ) .unique(); if (existing) { return; } await ctx.db.insert("workEvents", { createdAt: Date.now(), idempotencyKey, kind, payloadJson, referenceId, workId, }); }; export const recordArtifact = mutation({ args: { attemptId: v.optional(v.id("workAttempts")), designVersion: v.optional(v.number()), draft: v.object({ contentHash: v.optional(v.string()), environmentId: v.optional(v.string()), idempotencyKey: v.string(), kind: artifactKind, metadataJson: v.string(), producer: v.string(), provenanceJson: v.string(), sourceRevision: v.optional(v.string()), title: v.string(), uri: v.optional(v.string()), verificationStatus, }), runId: v.optional(v.id("workRuns")), sliceId: v.optional(v.string()), token: v.string(), workId: v.id("works"), }, handler: async (ctx, args) => { requireAgent(args.token); const work = await ctx.db.get(args.workId); if (!work) { throw new ConvexError("Work not found"); } const draft = await Effect.runPromise( decodeWorkArtifactDraft(args.draft) ).catch((error: unknown) => { throw new ConvexError( error instanceof Error ? error.message : "Invalid Work artifact" ); }); const attempt = args.attemptId ? await ctx.db.get(args.attemptId) : null; if (args.attemptId && !attempt) { throw new ConvexError("Attempt not found"); } const referencedRunId = args.runId ?? attempt?.runId; const run = referencedRunId ? await ctx.db.get(referencedRunId) : null; if (referencedRunId && !run) { throw new ConvexError("Run not found"); } if (run && run.workId !== work._id) { throw new ConvexError("Run does not belong to Work"); } if ( attempt && (attempt.workId !== work._id || (run !== null && attempt.runId !== run._id)) ) { throw new ConvexError("Attempt does not belong to Work Run"); } if ( run && ((args.designVersion !== undefined && args.designVersion !== run.designVersion) || (args.sliceId !== undefined && args.sliceId !== run.sliceId)) ) { throw new ConvexError("Artifact provenance conflicts with Work Run"); } const designVersion = args.designVersion ?? run?.designVersion; const sliceId = args.sliceId ?? run?.sliceId; if (designVersion !== undefined && sliceId !== undefined) { const slice = await ctx.db .query("workSlices") .withIndex("by_workId_and_designVersion_and_sliceId", (q) => q .eq("workId", work._id) .eq("designVersion", designVersion) .eq("sliceId", sliceId) ) .unique(); if (!slice) { throw new ConvexError("Artifact slice not found"); } } else if (designVersion !== undefined) { const design = await ctx.db .query("designPackets") .withIndex("by_work_and_version", (q) => q.eq("workId", work._id).eq("version", designVersion) ) .unique(); if (!design) { throw new ConvexError("Artifact Design not found"); } } const existing = await ctx.db .query("workArtifacts") .withIndex("by_workId_and_idempotencyKey", (q) => q.eq("workId", work._id).eq("idempotencyKey", draft.idempotencyKey) ) .unique(); if (existing) { const exactReplay = existing.kind === draft.kind && existing.title === draft.title && existing.uri === draft.uri && existing.contentHash === draft.contentHash && existing.sourceRevision === draft.sourceRevision && existing.environmentId === draft.environmentId && existing.producer === draft.producer && existing.provenanceJson === draft.provenanceJson && existing.metadataJson === draft.metadataJson && existing.verificationStatus === draft.verificationStatus && existing.designVersion === designVersion && existing.sliceId === sliceId && existing.runId === run?._id && existing.attemptId === attempt?._id; if (!exactReplay) { throw new ConvexError("Artifact idempotency key has conflicting data"); } return { artifactId: existing._id, created: false }; } const createdAt = Date.now(); const artifactId = await ctx.db.insert("workArtifacts", { ...draft, attemptId: attempt?._id, createdAt, designVersion, organizationId: work.organizationId, projectId: work.projectId, runId: run?._id, sliceId, workId: work._id, }); await appendEvent( ctx, work._id, "artifact.recorded", `artifact:${draft.idempotencyKey}`, String(artifactId), JSON.stringify({ kind: draft.kind, title: draft.title }) ); return { artifactId, created: true }; }, }); export const recordDelivery = mutation({ args: { artifactId: v.optional(v.id("workArtifacts")), draft: v.object({ externalId: v.string(), idempotencyKey: v.string(), kind: deliveryKind, metadataJson: v.string(), provider: v.string(), sourceRevision: v.string(), status: deliveryStatus, target: v.string(), url: v.optional(v.string()), }), token: v.string(), workId: v.id("works"), }, handler: async (ctx, args) => { requireAgent(args.token); const work = await ctx.db.get(args.workId); if (!work) { throw new ConvexError("Work not found"); } const draft = await Effect.runPromise( decodeWorkDeliveryDraft(args.draft) ).catch((error: unknown) => { throw new ConvexError( error instanceof Error ? error.message : "Invalid Work delivery" ); }); const artifact = args.artifactId ? await ctx.db.get(args.artifactId) : null; if (args.artifactId && !artifact) { throw new ConvexError("Artifact not found"); } if (artifact && artifact.workId !== work._id) { throw new ConvexError("Artifact does not belong to Work"); } const existing = await ctx.db .query("workDeliveries") .withIndex("by_workId_and_idempotencyKey", (q) => q.eq("workId", work._id).eq("idempotencyKey", draft.idempotencyKey) ) .unique(); if (existing) { const exactReplay = existing.kind === draft.kind && existing.provider === draft.provider && existing.externalId === draft.externalId && existing.url === draft.url && existing.sourceRevision === draft.sourceRevision && existing.target === draft.target && existing.status === draft.status && existing.metadataJson === draft.metadataJson && existing.artifactId === artifact?._id; if (!exactReplay) { throw new ConvexError("Delivery idempotency key has conflicting data"); } return { created: false, deliveryId: existing._id }; } const timestamp = Date.now(); const deliveryId = await ctx.db.insert("workDeliveries", { ...draft, artifactId: artifact?._id, createdAt: timestamp, organizationId: work.organizationId, projectId: work.projectId, updatedAt: timestamp, workId: work._id, }); await appendEvent( ctx, work._id, "delivery.recorded", `delivery:${draft.idempotencyKey}`, String(deliveryId), JSON.stringify({ kind: draft.kind, sourceRevision: draft.sourceRevision }) ); return { created: true, deliveryId }; }, }); export const updateDeliveryStatus = mutation({ args: { deliveryId: v.id("workDeliveries"), metadataJson: v.string(), status: deliveryStatus, token: v.string(), }, handler: async (ctx, args) => { requireAgent(args.token); // Validate JSON before persisting, matching recordDelivery's decode path. const metadata = JSON.parse(args.metadataJson) as unknown; if (typeof metadata !== "object" || metadata === null) { throw new ConvexError("Delivery metadata must be a JSON object"); } const delivery = await ctx.db.get(args.deliveryId); if (!delivery) { throw new ConvexError("Delivery not found"); } if (delivery.status === args.status) { return { changed: false, status: delivery.status }; } if (!canTransitionWorkDelivery(delivery.status, args.status)) { throw new ConvexError( `Invalid delivery transition: ${delivery.status} -> ${args.status}` ); } await ctx.db.patch(delivery._id, { metadataJson: args.metadataJson, status: args.status, updatedAt: Date.now(), }); await appendEvent( ctx, delivery.workId, "delivery.updated", `delivery-updated:${delivery._id}:${args.status}`, String(delivery._id), JSON.stringify({ from: delivery.status, to: args.status }) ); return { changed: true, status: args.status }; }, }); export const listForWork = query({ args: { workId: v.id("works") }, handler: async (ctx, args) => { const work = await ctx.db.get(args.workId); if (!work) { return null; } await requireProjectMember(ctx, work.projectId); const [artifacts, deliveries] = await Promise.all([ ctx.db .query("workArtifacts") .withIndex("by_workId_and_createdAt", (q) => q.eq("workId", work._id)) .order("desc") .take(100), ctx.db .query("workDeliveries") .withIndex("by_workId_and_createdAt", (q) => q.eq("workId", work._id)) .order("desc") .take(50), ]); return { artifacts, deliveries }; }, });