doatlas-2 / artifacts /api-server /src /lib /blueprint.ts
Iostream-Li's picture
Add files using upload-large-folder tool
5871090 verified
/**
* blueprint — Plan→Confirm→Execute two-phase persistence (Task #176).
*
* A Blueprint is a frozen record of "what the planner intends to do":
* - which problem_class
* - which network + active variant
* - the fully-specified input args
* - per-task seed list (so the runner is deterministic)
*
* The user (or the Reviewer) must approve a draft before the runner
* walks it. The blueprint id is referenced from result.zip's log, so
* audits can replay any past run.
*/
import { desc, eq } from "drizzle-orm";
import {
db,
executionPlans,
type ExecutionPlanRow,
type InsertExecutionPlanRow,
} from "@workspace/db";
import { newId } from "./ids";
export type PlanStatus =
| "draft"
| "approved"
| "running"
| "completed"
| "failed"
| "cancelled";
export interface BlueprintBody {
/** Network name (matches tool_networks.name). */
networkName: string;
/** Network input as the planner constructed it. */
input: Record<string, unknown>;
/** Per-task seed list — runner consumes (taskKey, params) tuples. */
tasks: Array<{ taskKey: string; params: Record<string, unknown> }>;
/** Planner notes for the audit trail. */
notes?: string;
}
export interface CreateBlueprintInput {
conversationId?: string | null;
ownerUserId?: string | null;
problemClassPath: string;
networkId: string;
versionId: string;
body: BlueprintBody;
notes?: string;
}
export async function createBlueprint(
input: CreateBlueprintInput,
): Promise<ExecutionPlanRow> {
const row: InsertExecutionPlanRow = {
id: newId("eplan"),
conversationId: input.conversationId ?? null,
ownerUserId: input.ownerUserId ?? null,
problemClassPath: input.problemClassPath,
networkId: input.networkId,
versionId: input.versionId,
blueprint: input.body,
status: "draft",
notes: input.notes ?? "",
};
const [inserted] = await db.insert(executionPlans).values(row).returning();
return inserted!;
}
export async function getBlueprint(
id: string,
): Promise<ExecutionPlanRow | null> {
const rows = await db
.select()
.from(executionPlans)
.where(eq(executionPlans.id, id))
.limit(1);
return rows[0] ?? null;
}
export async function listBlueprints(
ownerUserId?: string | null,
limit = 50,
): Promise<ExecutionPlanRow[]> {
let q = db.select().from(executionPlans).$dynamic();
if (ownerUserId) q = q.where(eq(executionPlans.ownerUserId, ownerUserId));
return q.orderBy(desc(executionPlans.createdAt)).limit(limit);
}
export async function approveBlueprint(
id: string,
approvedBy: string,
): Promise<ExecutionPlanRow> {
const [updated] = await db
.update(executionPlans)
.set({
status: "approved",
approvedBy,
approvedAt: new Date(),
updatedAt: new Date(),
})
.where(eq(executionPlans.id, id))
.returning();
if (!updated) throw new Error(`blueprint ${id} not found`);
return updated;
}
export async function setBlueprintStatus(
id: string,
status: PlanStatus,
patch?: { artifactPath?: string | null; notes?: string },
): Promise<ExecutionPlanRow> {
const [updated] = await db
.update(executionPlans)
.set({
status,
...(patch?.artifactPath !== undefined
? { outputArtifactPath: patch.artifactPath }
: {}),
...(patch?.notes !== undefined ? { notes: patch.notes } : {}),
updatedAt: new Date(),
})
.where(eq(executionPlans.id, id))
.returning();
if (!updated) throw new Error(`blueprint ${id} not found`);
return updated;
}
export function blueprintBody(row: ExecutionPlanRow): BlueprintBody {
return row.blueprint as BlueprintBody;
}