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>
This commit is contained in:
2026-07-21 19:09:54 +02:00
co-authored by Claude Sonnet 4.6
parent c51dd8cd67
commit d2e4934cca
63 changed files with 7035 additions and 2140 deletions
+109 -46
View File
@@ -4,7 +4,12 @@ import type { OutputTypeArtifactKind, OutputTypeWorkflowRolloutMode } from './ou
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 type WorkflowBlueprintType = 'cad_intake' | 'order_rendering' | 'still_graph_reference'
export type WorkflowBlueprintType =
| 'cad_intake'
| 'order_rendering'
| 'still_graph_reference'
| 'still_graph_alpha_reference'
| 'still_graph_blend_reference'
export type WorkflowCanonicalBlueprintType = WorkflowBlueprintType | 'starter_cad_intake' | 'starter_order_rendering'
export interface WorkflowRolloutLatestRun {
@@ -173,6 +178,8 @@ export interface WorkflowOrderLineContextOption {
value: string
label: string
meta: string
is_renderable: boolean
renderability_reason: string | null
}
export interface WorkflowOrderLineContextGroup {
@@ -379,49 +386,89 @@ function extractRenderParamsFromNodes(nodes: WorkflowNode[], step: string): Work
return normalizeRenderParams(match?.params ?? {})
}
function buildOrderLineStillGraphNodes(renderParams: WorkflowParams): { nodes: WorkflowNode[]; edges: WorkflowEdge[] } {
function buildOrderLineStillGraphNodes(
renderParams: WorkflowParams,
options: {
transparentBg?: boolean
includeBlendExport?: boolean
} = {},
): { nodes: WorkflowNode[]; edges: WorkflowEdge[] } {
const resolvedRenderParams = {
use_custom_render_settings: false,
...renderParams,
...(options.transparentBg !== undefined ? { transparent_bg: options.transparentBg } : {}),
}
const nodes: WorkflowNode[] = [
buildWorkflowNode('setup', 'order_line_setup', 0, 160, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 160, { label: 'Resolve Template' }),
buildWorkflowNode('populate_materials', 'auto_populate_materials', 220, 320, {
label: 'Auto Populate Materials',
type: 'processNode',
}),
buildWorkflowNode('bbox', 'glb_bbox', 220, 40, {
label: 'Compute Bounding Box',
type: 'processNode',
}),
buildWorkflowNode('resolve_materials', 'material_map_resolve', 440, 200, {
label: 'Resolve Material Map',
type: 'processNode',
}),
buildWorkflowNode('render', 'blender_still', 680, 160, {
label: 'Still Render',
type: 'renderNode',
params: resolvedRenderParams,
}),
buildWorkflowNode('output', 'output_save', 920, 120, {
label: 'Save Output',
type: 'outputNode',
}),
buildWorkflowNode('notify', 'notify', 920, 220, {
label: 'Notify Result',
type: 'outputNode',
}),
]
const edges: WorkflowEdge[] = [
{ 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' },
]
if (options.includeBlendExport) {
nodes.push(
buildWorkflowNode('blend_export', 'export_blend', 920, 360, {
label: 'Export Blend',
type: 'outputNode',
}),
buildWorkflowNode('save_blend', 'output_save', 1160, 320, {
label: 'Save Blend Output',
type: 'outputNode',
params: { expected_artifact_role: 'blend_export' },
}),
buildWorkflowNode('notify_blend', 'notify', 1160, 420, {
label: 'Notify Blend Export',
type: 'outputNode',
}),
)
edges.push(
{ from: 'setup', to: 'blend_export' },
{ from: 'template', to: 'blend_export' },
{ from: 'blend_export', to: 'save_blend' },
{ from: 'blend_export', to: 'notify_blend' },
)
}
return {
nodes: [
buildWorkflowNode('setup', 'order_line_setup', 0, 160, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 160, { label: 'Resolve Template' }),
buildWorkflowNode('populate_materials', 'auto_populate_materials', 220, 320, {
label: 'Auto Populate Materials',
type: 'processNode',
}),
buildWorkflowNode('bbox', 'glb_bbox', 220, 40, {
label: 'Compute Bounding Box',
type: 'processNode',
}),
buildWorkflowNode('resolve_materials', 'material_map_resolve', 440, 200, {
label: 'Resolve Material Map',
type: 'processNode',
}),
buildWorkflowNode('render', 'blender_still', 680, 160, {
label: 'Still Render',
type: 'renderNode',
params: { use_custom_render_settings: false, ...renderParams },
}),
buildWorkflowNode('output', 'output_save', 920, 120, {
label: 'Save Output',
type: 'outputNode',
}),
buildWorkflowNode('notify', 'notify', 920, 220, {
label: 'Notify Result',
type: 'outputNode',
}),
],
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' },
],
nodes,
edges,
}
}
@@ -681,12 +728,22 @@ export function buildWorkflowBlueprintConfig(blueprint: WorkflowBlueprintType):
}
}
const { nodes, edges } = buildOrderLineStillGraphNodes({
const stillReferenceParams: WorkflowParams = {
render_engine: 'cycles',
samples: 256,
width: 1920,
height: 1080,
})
}
const buildStillReference = () => {
if (blueprint === 'still_graph_alpha_reference') {
return buildOrderLineStillGraphNodes(stillReferenceParams, { transparentBg: true })
}
if (blueprint === 'still_graph_blend_reference') {
return buildOrderLineStillGraphNodes(stillReferenceParams, { includeBlendExport: true })
}
return buildOrderLineStillGraphNodes(stillReferenceParams)
}
const { nodes, edges } = buildStillReference()
return {
version: 1,
@@ -819,7 +876,13 @@ export function normalizeWorkflowConfig(raw: Record<string, unknown>): WorkflowC
}
}
if (rawUi.blueprint === 'cad_intake' || rawUi.blueprint === 'order_rendering' || rawUi.blueprint === 'still_graph_reference') {
if (
rawUi.blueprint === 'cad_intake' ||
rawUi.blueprint === 'order_rendering' ||
rawUi.blueprint === 'still_graph_reference' ||
rawUi.blueprint === 'still_graph_alpha_reference' ||
rawUi.blueprint === 'still_graph_blend_reference'
) {
const canonical = buildWorkflowBlueprintConfig(rawUi.blueprint)
return {
...canonical,