364 lines
8.8 KiB
TypeScript
364 lines
8.8 KiB
TypeScript
import type {
|
|
Role,
|
|
WorkflowStageCreateRequest,
|
|
WorkflowStagePart,
|
|
WorkflowStageReorderRequest,
|
|
WorkflowStageSummary,
|
|
WorkflowStageUpdateRequest,
|
|
WorkflowTemplateAuditEventSummary,
|
|
WorkflowTemplateCreateRequest,
|
|
WorkflowTemplateSummary,
|
|
WorkflowTemplateUpdateRequest,
|
|
} from "@pipeline/shared";
|
|
|
|
export type WorkflowDraft = {
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
};
|
|
|
|
export type WorkflowStageDraft = {
|
|
stableKey: string;
|
|
displayName: string;
|
|
description: string;
|
|
position: number;
|
|
ownerRole: Role;
|
|
runnerProfileKey: string;
|
|
requiredInputs: string[];
|
|
expectedOutputs: string[];
|
|
acceptanceCriteria: string[];
|
|
requiresHumanApproval: boolean;
|
|
retryPolicy: Record<string, unknown>;
|
|
parts: WorkflowStagePart[];
|
|
};
|
|
|
|
export type WorkflowStageFormPart = {
|
|
key: string;
|
|
type: string;
|
|
title: string;
|
|
payloadJson: string;
|
|
acceptanceCriteriaText: string;
|
|
};
|
|
|
|
export type WorkflowStageFormDraft = Omit<WorkflowStageDraft, "parts"> & {
|
|
requiredInputsText: string;
|
|
expectedOutputsText: string;
|
|
acceptanceCriteriaText: string;
|
|
retryPolicyJson: string;
|
|
parts: WorkflowStageFormPart[];
|
|
};
|
|
|
|
export type WorkflowStagePositionItem = {
|
|
id: string;
|
|
position: number;
|
|
};
|
|
|
|
export type AdminWorkflowRow = {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
status: string;
|
|
version: number;
|
|
updatedAt: string;
|
|
stageCount: number;
|
|
isDraft: boolean;
|
|
isActive: boolean;
|
|
isArchived: boolean;
|
|
};
|
|
|
|
export type AdminWorkflowAuditRow = {
|
|
id: string;
|
|
eventType: string;
|
|
createdAt: string;
|
|
actorUserId: string | null;
|
|
payload: string;
|
|
};
|
|
|
|
export type StageDraftValidationErrors = Partial<
|
|
Record<
|
|
| "stableKey"
|
|
| "displayName"
|
|
| "description"
|
|
| "runnerProfileKey"
|
|
| "retryPolicyJson"
|
|
| "parts",
|
|
string
|
|
>
|
|
>;
|
|
|
|
export function defaultWorkflowDraft(): WorkflowDraft {
|
|
return {
|
|
name: "",
|
|
slug: "",
|
|
description: "",
|
|
};
|
|
}
|
|
|
|
export function buildCreateWorkflowPayload(
|
|
draft: WorkflowDraft,
|
|
): WorkflowTemplateCreateRequest {
|
|
return {
|
|
name: draft.name,
|
|
slug: draft.slug,
|
|
description: draft.description,
|
|
};
|
|
}
|
|
|
|
export function buildUpdateWorkflowPayload(
|
|
draft: WorkflowDraft,
|
|
): WorkflowTemplateUpdateRequest {
|
|
return {
|
|
name: draft.name,
|
|
slug: draft.slug,
|
|
description: draft.description,
|
|
};
|
|
}
|
|
|
|
export function defaultWorkflowStagePart(): WorkflowStagePart {
|
|
return {
|
|
key: "stage-part",
|
|
type: "instruction",
|
|
title: "Stage instruction",
|
|
payload: {
|
|
prompt: "",
|
|
config: {},
|
|
},
|
|
acceptance_criteria: [],
|
|
};
|
|
}
|
|
|
|
export function defaultWorkflowStageDraft(position = 1): WorkflowStageDraft {
|
|
return {
|
|
stableKey: "",
|
|
displayName: "",
|
|
description: "",
|
|
position,
|
|
ownerRole: "EDITOR",
|
|
runnerProfileKey: "",
|
|
requiredInputs: [],
|
|
expectedOutputs: [],
|
|
acceptanceCriteria: [],
|
|
requiresHumanApproval: false,
|
|
retryPolicy: {
|
|
max_attempts: 1,
|
|
backoff_seconds: 0,
|
|
},
|
|
parts: [],
|
|
};
|
|
}
|
|
|
|
export function buildCreateWorkflowStagePayload(
|
|
draft: WorkflowStageDraft,
|
|
): WorkflowStageCreateRequest {
|
|
return {
|
|
stable_key: draft.stableKey,
|
|
display_name: draft.displayName,
|
|
description: draft.description,
|
|
position: draft.position,
|
|
owner_role: draft.ownerRole,
|
|
runner_profile_key: draft.runnerProfileKey,
|
|
required_inputs: draft.requiredInputs,
|
|
expected_outputs: draft.expectedOutputs,
|
|
acceptance_criteria: draft.acceptanceCriteria,
|
|
requires_human_approval: draft.requiresHumanApproval,
|
|
retry_policy: draft.retryPolicy,
|
|
parts: draft.parts,
|
|
};
|
|
}
|
|
|
|
export function buildUpdateWorkflowStagePayload(
|
|
draft: WorkflowStageDraft,
|
|
): WorkflowStageUpdateRequest {
|
|
return buildCreateWorkflowStagePayload(draft);
|
|
}
|
|
|
|
export function moveWorkflowStage<TStage extends WorkflowStagePositionItem>(
|
|
stages: readonly TStage[],
|
|
stageId: string,
|
|
targetIndex: number,
|
|
): TStage[] {
|
|
const currentIndex = stages.findIndex((stage) => stage.id === stageId);
|
|
if (currentIndex === -1) {
|
|
return stages.map((stage, index) => ({
|
|
...stage,
|
|
position: index + 1,
|
|
}));
|
|
}
|
|
|
|
const nextStages = [...stages];
|
|
const [stage] = nextStages.splice(currentIndex, 1);
|
|
const clampedIndex = Math.max(0, Math.min(targetIndex, nextStages.length));
|
|
nextStages.splice(clampedIndex, 0, stage);
|
|
|
|
return nextStages.map((nextStage, index) => ({
|
|
...nextStage,
|
|
position: index + 1,
|
|
}));
|
|
}
|
|
|
|
export function buildReorderWorkflowStagesPayload(
|
|
stages: readonly WorkflowStagePositionItem[],
|
|
): WorkflowStageReorderRequest {
|
|
return {
|
|
stage_ids: stages.map((stage) => stage.id),
|
|
};
|
|
}
|
|
|
|
export function buildAdminWorkflowRows(
|
|
workflows: readonly WorkflowTemplateSummary[],
|
|
): AdminWorkflowRow[] {
|
|
return workflows.map((workflow) => ({
|
|
id: workflow.id,
|
|
name: workflow.name,
|
|
slug: workflow.slug,
|
|
status: workflow.status,
|
|
version: workflow.version,
|
|
updatedAt: workflow.updated_at,
|
|
stageCount: workflow.stages?.length ?? 0,
|
|
isDraft: workflow.status === "DRAFT",
|
|
isActive: workflow.status === "ACTIVE",
|
|
isArchived: workflow.status === "ARCHIVED",
|
|
}));
|
|
}
|
|
|
|
export function buildAdminWorkflowAuditRows(
|
|
events: readonly WorkflowTemplateAuditEventSummary[],
|
|
): AdminWorkflowAuditRow[] {
|
|
return events.map((event) => ({
|
|
id: event.id,
|
|
eventType: event.event_type,
|
|
createdAt: event.created_at,
|
|
actorUserId: event.actor_user_id ?? null,
|
|
payload: JSON.stringify(event.payload ?? {}),
|
|
}));
|
|
}
|
|
|
|
export function workflowDraftFromSummary(
|
|
workflow: WorkflowTemplateSummary,
|
|
): WorkflowDraft {
|
|
return {
|
|
name: workflow.name,
|
|
slug: workflow.slug,
|
|
description: workflow.description,
|
|
};
|
|
}
|
|
|
|
export function workflowStageDraftFromSummary(
|
|
stage: WorkflowStageSummary,
|
|
): WorkflowStageDraft {
|
|
return {
|
|
stableKey: stage.stable_key,
|
|
displayName: stage.display_name,
|
|
description: stage.description,
|
|
position: stage.position,
|
|
ownerRole: stage.owner_role,
|
|
runnerProfileKey: stage.runner_profile_key,
|
|
requiredInputs: stage.required_inputs ?? [],
|
|
expectedOutputs: stage.expected_outputs ?? [],
|
|
acceptanceCriteria: stage.acceptance_criteria ?? [],
|
|
requiresHumanApproval: stage.requires_human_approval ?? false,
|
|
retryPolicy: stage.retry_policy ?? {},
|
|
parts: stage.parts ?? [],
|
|
};
|
|
}
|
|
|
|
export function workflowStageFormDraftFromSummary(
|
|
stage: WorkflowStageSummary,
|
|
): WorkflowStageFormDraft {
|
|
const draft = workflowStageDraftFromSummary(stage);
|
|
return workflowStageDraftToFormDraft(draft);
|
|
}
|
|
|
|
export function workflowStageDraftToFormDraft(
|
|
draft: WorkflowStageDraft,
|
|
): WorkflowStageFormDraft {
|
|
return {
|
|
...draft,
|
|
requiredInputsText: draft.requiredInputs.join("\n"),
|
|
expectedOutputsText: draft.expectedOutputs.join("\n"),
|
|
acceptanceCriteriaText: draft.acceptanceCriteria.join("\n"),
|
|
retryPolicyJson: JSON.stringify(draft.retryPolicy, null, 2),
|
|
parts: draft.parts.map((part) => ({
|
|
key: part.key,
|
|
type: part.type,
|
|
title: part.title,
|
|
payloadJson: JSON.stringify(part.payload ?? {}, null, 2),
|
|
acceptanceCriteriaText: (part.acceptance_criteria ?? []).join("\n"),
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function defaultWorkflowStageFormDraft(
|
|
position = 1,
|
|
): WorkflowStageFormDraft {
|
|
return workflowStageDraftToFormDraft(defaultWorkflowStageDraft(position));
|
|
}
|
|
|
|
export function workflowStageFormDraftToStageDraft(
|
|
draft: WorkflowStageFormDraft,
|
|
): WorkflowStageDraft | null {
|
|
const retryPolicy = parseJsonObject(draft.retryPolicyJson);
|
|
if (retryPolicy === null) {
|
|
return null;
|
|
}
|
|
|
|
const parts: WorkflowStagePart[] = [];
|
|
for (const part of draft.parts) {
|
|
const payload = parseJsonObject(part.payloadJson);
|
|
if (payload === null) {
|
|
return null;
|
|
}
|
|
parts.push({
|
|
key: part.key,
|
|
type: part.type,
|
|
title: part.title,
|
|
payload,
|
|
acceptance_criteria: splitLines(part.acceptanceCriteriaText),
|
|
});
|
|
}
|
|
|
|
return {
|
|
stableKey: draft.stableKey,
|
|
displayName: draft.displayName,
|
|
description: draft.description,
|
|
position: draft.position,
|
|
ownerRole: draft.ownerRole,
|
|
runnerProfileKey: draft.runnerProfileKey,
|
|
requiredInputs: splitLines(draft.requiredInputsText),
|
|
expectedOutputs: splitLines(draft.expectedOutputsText),
|
|
acceptanceCriteria: splitLines(draft.acceptanceCriteriaText),
|
|
requiresHumanApproval: draft.requiresHumanApproval,
|
|
retryPolicy,
|
|
parts,
|
|
};
|
|
}
|
|
|
|
export function splitLines(value: string): string[] {
|
|
return value
|
|
.split(/\r?\n/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function parseJsonObject(value: string): Record<string, unknown> | null {
|
|
if (!value.trim()) {
|
|
return {};
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (
|
|
parsed &&
|
|
typeof parsed === "object" &&
|
|
!Array.isArray(parsed) &&
|
|
parsed !== null
|
|
) {
|
|
return parsed as Record<string, unknown>;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|