feat: harden workflow graph contracts
This commit is contained in:
+151
-39
@@ -2,12 +2,14 @@ import api from './client'
|
||||
|
||||
export type WorkflowPresetType = 'still' | 'still_graph' | 'turntable' | 'multi_angle' | 'still_with_exports' | 'custom'
|
||||
export type WorkflowExecutionMode = 'legacy' | 'graph' | 'shadow'
|
||||
export type WorkflowStarterFamily = 'cad_file' | 'order_line'
|
||||
|
||||
export interface WorkflowDefinition {
|
||||
id: string
|
||||
name: string
|
||||
output_type_id: string | null
|
||||
config: WorkflowConfig
|
||||
family: WorkflowNodeFamily | 'mixed' | null
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
}
|
||||
@@ -54,6 +56,7 @@ export interface WorkflowEdge {
|
||||
export interface WorkflowUi {
|
||||
preset?: WorkflowPresetType
|
||||
execution_mode?: WorkflowExecutionMode
|
||||
family?: WorkflowNodeFamily | 'mixed'
|
||||
blueprint?: string
|
||||
}
|
||||
|
||||
@@ -234,9 +237,13 @@ export interface WorkflowNodeFieldDefinition {
|
||||
options: WorkflowNodeFieldOption[]
|
||||
}
|
||||
|
||||
export type WorkflowNodeFamily = 'cad_file' | 'order_line'
|
||||
|
||||
export interface WorkflowNodeDefinition {
|
||||
step: string
|
||||
label: string
|
||||
family: WorkflowNodeFamily
|
||||
module_key: string
|
||||
category: StepCategory
|
||||
description: string
|
||||
node_type: string
|
||||
@@ -245,6 +252,11 @@ export interface WorkflowNodeDefinition {
|
||||
fields: WorkflowNodeFieldDefinition[]
|
||||
execution_kind: WorkflowNodeExecutionKind
|
||||
legacy_compatible: boolean
|
||||
input_contract: Record<string, unknown>
|
||||
output_contract: Record<string, unknown>
|
||||
artifact_roles_produced: string[]
|
||||
artifact_roles_consumed: string[]
|
||||
legacy_source: string | null
|
||||
}
|
||||
|
||||
export interface WorkflowNodeDefinitionsResponse {
|
||||
@@ -268,6 +280,48 @@ export const getNodeDefinitions = (): Promise<WorkflowNodeDefinitionsResponse> =
|
||||
export const getPipelineSteps = (): Promise<PipelineStepsResponse> =>
|
||||
api.get('/workflows/pipeline-steps').then(r => r.data)
|
||||
|
||||
function buildStillGraphNodes(renderParams: WorkflowParams): { nodes: WorkflowNode[]; edges: WorkflowEdge[] } {
|
||||
return {
|
||||
nodes: [
|
||||
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 160 } } },
|
||||
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 160 } } },
|
||||
{
|
||||
id: 'populate_materials',
|
||||
step: 'auto_populate_materials',
|
||||
params: {},
|
||||
ui: { type: 'processNode', label: 'Auto Populate Materials', position: { x: 220, y: 320 } },
|
||||
},
|
||||
{ id: 'bbox', step: 'glb_bbox', params: {}, ui: { type: 'processNode', label: 'Compute Bounding Box', position: { x: 220, y: 40 } } },
|
||||
{
|
||||
id: 'resolve_materials',
|
||||
step: 'material_map_resolve',
|
||||
params: {},
|
||||
ui: { type: 'processNode', label: 'Resolve Material Map', position: { x: 440, y: 200 } },
|
||||
},
|
||||
{
|
||||
id: 'render',
|
||||
step: 'blender_still',
|
||||
params: { use_custom_render_settings: true, ...renderParams },
|
||||
ui: { type: 'renderNode', label: 'Still Render', position: { x: 680, y: 160 } },
|
||||
},
|
||||
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 920, y: 120 } } },
|
||||
{ id: 'notify', step: 'notify', params: {}, ui: { type: 'outputNode', label: 'Notify Result', position: { x: 920, y: 220 } } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'setup', to: 'template' },
|
||||
{ from: 'setup', to: 'populate_materials' },
|
||||
{ from: 'setup', to: 'bbox' },
|
||||
{ from: 'template', to: 'resolve_materials' },
|
||||
{ from: 'populate_materials', to: 'resolve_materials' },
|
||||
{ from: 'resolve_materials', to: 'render' },
|
||||
{ from: 'bbox', to: 'render' },
|
||||
{ from: 'template', to: 'render' },
|
||||
{ from: 'render', to: 'output' },
|
||||
{ from: 'render', to: 'notify' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams = {}): WorkflowConfig {
|
||||
const renderParams = { ...params }
|
||||
const resolution = Array.isArray(renderParams.resolution) ? renderParams.resolution : undefined
|
||||
@@ -280,7 +334,7 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
||||
if (type === 'still') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type, execution_mode: 'legacy' },
|
||||
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
|
||||
nodes: [
|
||||
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
|
||||
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 100 } } },
|
||||
@@ -296,43 +350,19 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
||||
}
|
||||
|
||||
if (type === 'still_graph') {
|
||||
const { nodes, edges } = buildStillGraphNodes(renderParams)
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type, execution_mode: 'graph' },
|
||||
nodes: [
|
||||
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
|
||||
{
|
||||
id: 'populate_materials',
|
||||
step: 'auto_populate_materials',
|
||||
params: {},
|
||||
ui: { type: 'processNode', label: 'Auto Populate Materials', position: { x: 220, y: 100 } },
|
||||
},
|
||||
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 440, y: 100 } } },
|
||||
{
|
||||
id: 'resolve_materials',
|
||||
step: 'material_map_resolve',
|
||||
params: {},
|
||||
ui: { type: 'processNode', label: 'Resolve Material Map', position: { x: 660, y: 100 } },
|
||||
},
|
||||
{ id: 'render', step: 'blender_still', params: renderParams, ui: { type: 'renderNode', label: 'Still Render', position: { x: 880, y: 100 } } },
|
||||
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 1100, y: 70 } } },
|
||||
{ id: 'notify', step: 'notify', params: {}, ui: { type: 'outputNode', label: 'Notify Result', position: { x: 1100, y: 160 } } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'setup', to: 'populate_materials' },
|
||||
{ from: 'populate_materials', to: 'template' },
|
||||
{ from: 'template', to: 'resolve_materials' },
|
||||
{ from: 'resolve_materials', to: 'render' },
|
||||
{ from: 'render', to: 'output' },
|
||||
{ from: 'render', to: 'notify' },
|
||||
],
|
||||
ui: { preset: type, execution_mode: 'graph', family: 'order_line' },
|
||||
nodes,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'turntable') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type, execution_mode: 'legacy' },
|
||||
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
|
||||
nodes: [
|
||||
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
|
||||
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 100 } } },
|
||||
@@ -353,7 +383,7 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
||||
delete sharedParams.angles
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type, execution_mode: 'legacy' },
|
||||
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
|
||||
nodes: [
|
||||
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 195 } } },
|
||||
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 195 } } },
|
||||
@@ -376,7 +406,7 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
||||
if (type === 'still_with_exports') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type, execution_mode: 'legacy' },
|
||||
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
|
||||
nodes: [
|
||||
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
|
||||
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 100 } } },
|
||||
@@ -395,7 +425,7 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: 'custom', execution_mode: 'legacy' },
|
||||
ui: { preset: 'custom', execution_mode: 'legacy', family: 'order_line' },
|
||||
nodes: [
|
||||
{
|
||||
id: 'setup',
|
||||
@@ -409,25 +439,30 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
||||
}
|
||||
|
||||
function normalizeWorkflowDefinition(raw: WorkflowDefinition): WorkflowDefinition {
|
||||
const config = normalizeWorkflowConfig(raw.config as unknown as Record<string, unknown>)
|
||||
return {
|
||||
...raw,
|
||||
config: normalizeWorkflowConfig(raw.config as unknown as Record<string, unknown>),
|
||||
family: raw.family ?? inferWorkflowFamily(config),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeWorkflowConfig(raw: Record<string, unknown>): WorkflowConfig {
|
||||
if ('version' in raw && Array.isArray(raw.nodes)) {
|
||||
const rawUi = (raw.ui as WorkflowUi | undefined) ?? {}
|
||||
const nodes = (raw.nodes as WorkflowNode[]).map(node => ({
|
||||
...node,
|
||||
params: { ...(node.params ?? {}) },
|
||||
}))
|
||||
const edges = Array.isArray(raw.edges) ? (raw.edges as WorkflowEdge[]) : []
|
||||
return {
|
||||
version: Number(raw.version ?? 1),
|
||||
nodes: (raw.nodes as WorkflowNode[]).map(node => ({
|
||||
...node,
|
||||
params: { ...(node.params ?? {}) },
|
||||
})),
|
||||
edges: Array.isArray(raw.edges) ? (raw.edges as WorkflowEdge[]) : [],
|
||||
nodes,
|
||||
edges,
|
||||
ui: {
|
||||
...rawUi,
|
||||
execution_mode: rawUi.execution_mode ?? 'legacy',
|
||||
family: rawUi.family ?? inferWorkflowFamily({ version: Number(raw.version ?? 1), nodes, edges }),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -448,6 +483,83 @@ export function createPresetWorkflowConfig(type: WorkflowPresetType, params: Wor
|
||||
return migratePresetConfig(type, params)
|
||||
}
|
||||
|
||||
export function createStarterWorkflowConfig(family: WorkflowStarterFamily = 'order_line'): WorkflowConfig {
|
||||
if (family === 'cad_file') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: {
|
||||
preset: 'custom',
|
||||
execution_mode: 'legacy',
|
||||
family: 'cad_file',
|
||||
blueprint: 'starter_cad_intake',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 'resolve_step',
|
||||
step: 'resolve_step_path',
|
||||
params: {},
|
||||
ui: { type: 'inputNode', label: 'Resolve STEP Path', position: { x: 120, y: 140 } },
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
ui: {
|
||||
preset: 'custom',
|
||||
execution_mode: 'legacy',
|
||||
family: 'order_line',
|
||||
blueprint: 'starter_order_rendering',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 'setup',
|
||||
step: 'order_line_setup',
|
||||
params: {},
|
||||
ui: { type: 'processNode', label: 'Order Line Setup', position: { x: 120, y: 140 } },
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkflowPresetType(config: WorkflowConfig): WorkflowPresetType {
|
||||
return config.ui?.preset ?? 'custom'
|
||||
}
|
||||
|
||||
export function inferWorkflowFamily(config: WorkflowConfig): WorkflowNodeFamily | 'mixed' | null {
|
||||
const families = new Set(
|
||||
config.nodes
|
||||
.map(node => {
|
||||
switch (node.step) {
|
||||
case 'resolve_step_path':
|
||||
case 'occ_object_extract':
|
||||
case 'occ_glb_export':
|
||||
case 'stl_cache_generate':
|
||||
case 'blender_render':
|
||||
case 'threejs_render':
|
||||
case 'thumbnail_save':
|
||||
return 'cad_file'
|
||||
case 'order_line_setup':
|
||||
case 'resolve_template':
|
||||
case 'material_map_resolve':
|
||||
case 'auto_populate_materials':
|
||||
case 'glb_bbox':
|
||||
case 'blender_still':
|
||||
case 'blender_turntable':
|
||||
case 'output_save':
|
||||
case 'export_blend':
|
||||
case 'notify':
|
||||
return 'order_line'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((family): family is WorkflowNodeFamily => family !== null),
|
||||
)
|
||||
if (families.size === 0) return null
|
||||
if (families.size > 1) return 'mixed'
|
||||
return Array.from(families)[0]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user