Files
HartOMat/frontend/src/components/workflows/workflowReferenceBundles.ts
T
HartmutandClaude Sonnet 4.6 d2e4934cca feat: workflow graph system — blocks 1-20 complete checkpoint
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>
2026-07-21 19:09:54 +02:00

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,
}
}