Full graph-based workflow execution engine with: - WorkflowGraphRuntime with two-phase dispatch (collect → fire Celery tasks) - Node registry with contract validation, socket types, execution kinds - WorkflowRuntimeServices: preflight, context resolution, shadow-mode A/B - WorkflowRouter: CRUD + dispatch/preflight/run-history API endpoints - OutputTypeContracts: workflow binding, rollout-mode resolution - Admin router: output-type workflow binding endpoints - Frontend: complete workflow editor with drag-drop canvas, node inspector, module bundles, reference bundles, preflight panel, validation banner, authoring guidance, blueprint templates, shadow/rollout gate UI - Tests: comprehensive coverage for all workflow modules - Docs: NODE_CONTRACT_AUDIT and VALIDATION_ERROR_INVENTORY All 20 blocks from NEXT_20_BLOCK_BATCH_PLAN completed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
248 lines
7.8 KiB
TypeScript
248 lines
7.8 KiB
TypeScript
import type { Edge, Node } from '@xyflow/react'
|
|
|
|
import {
|
|
buildWorkflowBlueprintConfig,
|
|
type WorkflowNodeDefinition,
|
|
type WorkflowConfig,
|
|
type WorkflowParams,
|
|
} from '../../api/workflows'
|
|
import {
|
|
inferNodeLabel,
|
|
inferNodeType,
|
|
normalizeWorkflowParams,
|
|
type WorkflowCanvasNodeData,
|
|
} from './workflowGraphDraft'
|
|
import type { WorkflowGraphFamily } from './workflowNodeLibrary'
|
|
|
|
export type WorkflowReferenceBundleId =
|
|
| 'still_render_reference'
|
|
| 'still_render_alpha_reference'
|
|
| 'still_render_blend_reference'
|
|
| 'cad_intake_reference'
|
|
|
|
export type WorkflowReferenceBundleDefinition = {
|
|
id: WorkflowReferenceBundleId
|
|
label: string
|
|
shortLabel: string
|
|
description: string
|
|
family: Exclude<WorkflowGraphFamily, 'mixed'>
|
|
stepIds: string[]
|
|
stage: string
|
|
}
|
|
|
|
type WorkflowReferenceBundleInsertionResult =
|
|
| {
|
|
ok: true
|
|
bundle: WorkflowReferenceBundleDefinition
|
|
nodes: Node[]
|
|
edges: Edge[]
|
|
}
|
|
| {
|
|
ok: false
|
|
reason: string
|
|
}
|
|
|
|
const WORKFLOW_REFERENCE_BUNDLE_REGISTRY: WorkflowReferenceBundleDefinition[] = [
|
|
{
|
|
id: 'cad_intake_reference',
|
|
label: 'CAD Intake Reference',
|
|
shortLabel: 'CAD Intake Reference',
|
|
description: 'Insert the canonical CAD intake path so extraction, geometry export, STL cache generation, and thumbnail publishing stay wired in a known-good order.',
|
|
family: 'cad_file',
|
|
stepIds: [
|
|
'resolve_step_path',
|
|
'occ_object_extract',
|
|
'occ_glb_export',
|
|
'stl_cache_generate',
|
|
'thumbnail_save',
|
|
],
|
|
stage: 'Reference Path',
|
|
},
|
|
{
|
|
id: 'still_render_reference',
|
|
label: 'Still Render Reference',
|
|
shortLabel: 'Still Reference',
|
|
description: 'Insert the complete canonical non-legacy still-render path, including material resolution, render, output, and notification branches.',
|
|
family: 'order_line',
|
|
stepIds: [
|
|
'order_line_setup',
|
|
'resolve_template',
|
|
'auto_populate_materials',
|
|
'glb_bbox',
|
|
'material_map_resolve',
|
|
'blender_still',
|
|
'output_save',
|
|
'notify',
|
|
],
|
|
stage: 'Reference Path',
|
|
},
|
|
{
|
|
id: 'still_render_alpha_reference',
|
|
label: 'Still Render Alpha Reference',
|
|
shortLabel: 'Still Alpha',
|
|
description: 'Insert the canonical still-render path with transparent-alpha defaults so compositing-ready PNG outputs stay workflow-first.',
|
|
family: 'order_line',
|
|
stepIds: [
|
|
'order_line_setup',
|
|
'resolve_template',
|
|
'auto_populate_materials',
|
|
'glb_bbox',
|
|
'material_map_resolve',
|
|
'blender_still',
|
|
'output_save',
|
|
'notify',
|
|
],
|
|
stage: 'Reference Path',
|
|
},
|
|
{
|
|
id: 'still_render_blend_reference',
|
|
label: 'Still Render + Blend Reference',
|
|
shortLabel: 'Still + Blend',
|
|
description: 'Insert the canonical still-render path plus explicit .blend export delivery nodes so still and blend output-types can bind to the same graph family cleanly.',
|
|
family: 'order_line',
|
|
stepIds: [
|
|
'order_line_setup',
|
|
'resolve_template',
|
|
'auto_populate_materials',
|
|
'glb_bbox',
|
|
'material_map_resolve',
|
|
'blender_still',
|
|
'output_save',
|
|
'notify',
|
|
'export_blend',
|
|
],
|
|
stage: 'Reference Path',
|
|
},
|
|
]
|
|
|
|
function buildNodeData(
|
|
step: string,
|
|
params: WorkflowParams = {},
|
|
definition?: WorkflowNodeDefinition,
|
|
overrides?: Partial<WorkflowCanvasNodeData>,
|
|
): WorkflowCanvasNodeData {
|
|
return {
|
|
label: overrides?.label ?? definition?.label ?? inferNodeLabel(step),
|
|
params: normalizeWorkflowParams(params),
|
|
step,
|
|
description: overrides?.description ?? definition?.description,
|
|
icon: overrides?.icon ?? definition?.icon,
|
|
category: overrides?.category ?? definition?.category,
|
|
}
|
|
}
|
|
|
|
function getReferenceTemplate(bundleId: WorkflowReferenceBundleId) {
|
|
const toTemplate = (config: WorkflowConfig) => ({
|
|
nodes: config.nodes,
|
|
edges: config.edges,
|
|
})
|
|
|
|
switch (bundleId) {
|
|
case 'cad_intake_reference':
|
|
return toTemplate(buildWorkflowBlueprintConfig('cad_intake'))
|
|
case 'still_render_reference':
|
|
return toTemplate(buildWorkflowBlueprintConfig('still_graph_reference'))
|
|
case 'still_render_alpha_reference':
|
|
return toTemplate(buildWorkflowBlueprintConfig('still_graph_alpha_reference'))
|
|
case 'still_render_blend_reference':
|
|
return toTemplate(buildWorkflowBlueprintConfig('still_graph_blend_reference'))
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function getWorkflowReferenceBundle(bundleId: WorkflowReferenceBundleId) {
|
|
return WORKFLOW_REFERENCE_BUNDLE_REGISTRY.find(bundle => bundle.id === bundleId) ?? null
|
|
}
|
|
|
|
export function getWorkflowReferenceBundles(
|
|
nodeDefinitions: WorkflowNodeDefinition[],
|
|
graphFamily: WorkflowGraphFamily,
|
|
): WorkflowReferenceBundleDefinition[] {
|
|
const availableSteps = new Set(nodeDefinitions.map(definition => definition.step))
|
|
|
|
return WORKFLOW_REFERENCE_BUNDLE_REGISTRY.filter(bundle => {
|
|
if (graphFamily !== 'mixed' && bundle.family !== graphFamily) return false
|
|
return bundle.stepIds.every(step => availableSteps.has(step))
|
|
})
|
|
}
|
|
|
|
export function createWorkflowReferenceBundleInsertion(args: {
|
|
bundleId: WorkflowReferenceBundleId
|
|
graphFamily: WorkflowGraphFamily
|
|
nodeDefinitionsByStep: Record<string, WorkflowNodeDefinition>
|
|
existingNodes: Node[]
|
|
preferredPosition?: { x: number; y: number }
|
|
}): WorkflowReferenceBundleInsertionResult {
|
|
const { bundleId, graphFamily, nodeDefinitionsByStep, existingNodes, preferredPosition } = args
|
|
const bundle = getWorkflowReferenceBundle(bundleId)
|
|
|
|
if (!bundle) {
|
|
return { ok: false, reason: 'Unknown workflow reference path.' }
|
|
}
|
|
|
|
if (graphFamily !== 'mixed' && bundle.family !== graphFamily) {
|
|
return { ok: false, reason: `${bundle.label} does not belong to the active authoring family.` }
|
|
}
|
|
|
|
const missingStep = bundle.stepIds.find(step => !nodeDefinitionsByStep[step])
|
|
if (missingStep) {
|
|
return { ok: false, reason: `Workflow reference path is missing definition for ${missingStep}.` }
|
|
}
|
|
|
|
const template = getReferenceTemplate(bundleId)
|
|
if (!template) {
|
|
return { ok: false, reason: 'Workflow reference path template is unavailable.' }
|
|
}
|
|
|
|
const existingMaxX = existingNodes.length > 0 ? Math.max(...existingNodes.map(node => node.position.x)) : null
|
|
const existingMaxY = existingNodes.length > 0 ? Math.max(...existingNodes.map(node => node.position.y)) : null
|
|
const anchorX = preferredPosition?.x ?? (existingMaxX !== null ? existingMaxX + 260 : 120)
|
|
const anchorY = preferredPosition?.y ?? (existingMaxY !== null ? existingMaxY + 80 : 120)
|
|
const timestamp = Date.now()
|
|
|
|
const templatePositions = template.nodes.map(node => node.ui?.position ?? { x: 0, y: 0 })
|
|
const minX = Math.min(...templatePositions.map(position => position.x))
|
|
const minY = Math.min(...templatePositions.map(position => position.y))
|
|
|
|
const nodeIdMap = new Map<string, string>()
|
|
const nodes = template.nodes.map(node => {
|
|
const definition = nodeDefinitionsByStep[node.step]
|
|
const templatePosition = node.ui?.position ?? { x: 0, y: 0 }
|
|
const id = `${bundle.id}_${node.id}_${timestamp}`
|
|
nodeIdMap.set(node.id, id)
|
|
|
|
return {
|
|
id,
|
|
type: definition?.node_type ?? node.ui?.type ?? inferNodeType(node.step),
|
|
position: {
|
|
x: anchorX + (templatePosition.x - minX),
|
|
y: anchorY + (templatePosition.y - minY),
|
|
},
|
|
data: buildNodeData(
|
|
node.step,
|
|
node.params ?? definition?.defaults ?? {},
|
|
definition,
|
|
node.ui?.label
|
|
? {
|
|
label: node.ui.label,
|
|
}
|
|
: undefined,
|
|
),
|
|
} satisfies Node
|
|
})
|
|
|
|
const edges = template.edges.map(edge => ({
|
|
id: `${nodeIdMap.get(edge.from)}->${nodeIdMap.get(edge.to)}`,
|
|
source: nodeIdMap.get(edge.from) ?? edge.from,
|
|
target: nodeIdMap.get(edge.to) ?? edge.to,
|
|
}) satisfies Edge)
|
|
|
|
return {
|
|
ok: true,
|
|
bundle,
|
|
nodes,
|
|
edges,
|
|
}
|
|
}
|