Files
HartOMat/frontend/src/components/workflows/workflowAuthoringGuidance.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

297 lines
9.7 KiB
TypeScript

import type { WorkflowNodeDefinition } from '../../api/workflows'
import type { WorkflowGraphFamily } from './workflowNodeLibrary'
import { getWorkflowModuleBundles, type WorkflowModuleBundleDefinition, type WorkflowModuleBundleId } from './workflowModuleBundles'
import {
getWorkflowReferenceBundles,
type WorkflowReferenceBundleDefinition,
type WorkflowReferenceBundleId,
} from './workflowReferenceBundles'
export const STARTER_NODE_STEP_ORDER: Record<WorkflowGraphFamily, string[]> = {
cad_file: [
'resolve_step_path',
'occ_object_extract',
'occ_glb_export',
'stl_cache_generate',
'thumbnail_save',
],
order_line: [
'order_line_setup',
'resolve_template',
'auto_populate_materials',
'glb_bbox',
'material_map_resolve',
'blender_still',
'output_save',
'notify',
],
mixed: [],
}
export const STARTER_PATH_TITLES: Record<WorkflowGraphFamily, string> = {
cad_file: 'CAD intake assembly',
order_line: 'Still-render assembly',
mixed: 'Starter path',
}
export const STARTER_PATH_DESCRIPTIONS: Record<WorkflowGraphFamily, string> = {
cad_file: 'Use this module sequence to take a CAD file through extraction, preview generation, and published output.',
order_line: 'Use this module sequence to keep the non-legacy still graph parallel to the legacy render path.',
mixed: 'Reference sequence for assembling a workflow graph.',
}
export type WorkflowAuthoringPriority = {
title: string
description: string
}
export type WorkflowAuthoringFlowStep = {
index: number
title: string
description: string
}
export type WorkflowAuthoringBundleStatus<TBundle> = TBundle & {
presentCount: number
totalCount: number
}
export type WorkflowStarterPathItem = {
index: number
definition: WorkflowNodeDefinition
isPresent: boolean
}
export type WorkflowAuthoringStageProgress = {
id: string
title: string
description: string
present: number
total: number
actionLabel: string
actionKind: 'reference' | 'module' | 'step'
bundleId?: WorkflowReferenceBundleId | WorkflowModuleBundleId
step?: string
}
export type WorkflowAuthoringPlan = {
title: string
description: string
priorities: WorkflowAuthoringPriority[]
authoringFlow: WorkflowAuthoringFlowStep[]
referenceBundles: WorkflowAuthoringBundleStatus<WorkflowReferenceBundleDefinition>[]
moduleBundles: WorkflowAuthoringBundleStatus<WorkflowModuleBundleDefinition>[]
starterTitle: string
starterDescription: string
starterItems: WorkflowStarterPathItem[]
starterCompletedCount: number
stageProgress: WorkflowAuthoringStageProgress[]
gapFillDefinitions: WorkflowNodeDefinition[]
}
function haveSameStepSet(left: string[], right: string[]) {
if (left.length !== right.length) return false
const rightSet = new Set(right)
return left.every(step => rightSet.has(step))
}
function getAuthoringPriorities(graphFamily: WorkflowGraphFamily): WorkflowAuthoringPriority[] {
if (graphFamily === 'order_line') {
return [
{
title: 'Start with the Still Render Reference path',
description: 'Insert the full non-legacy still production baseline first, then tune modules or individual nodes.',
},
{
title: 'Swap stages with stage modules',
description: 'Use scene-prep, materials, render, and publish bundles to change whole production stages without breaking graph-safe sequencing.',
},
{
title: 'Use raw nodes last',
description: 'Drop to legacy, bridge, or native graph nodes only after the production path already exists on canvas.',
},
]
}
if (graphFamily === 'cad_file') {
return [
{
title: 'Start with the CAD intake assembly',
description: 'Build the import and preview chain first so downstream consumers always receive consistent artifacts.',
},
{
title: 'Extend with stage bundles',
description: 'Use reusable intake modules before inserting isolated conversion or utility nodes.',
},
{
title: 'Use raw nodes last',
description: 'Only drop to individual nodes for edge cases or targeted debugging once the intake baseline is present.',
},
]
}
return [
{
title: 'Split the graph by responsibility',
description: 'Keep mixed graphs organized by building complete stages first, then add isolated nodes only for orchestration glue.',
},
{
title: 'Prefer reusable modules over hand-wiring',
description: 'Use grouped bundles whenever a stage can be expressed as a reusable production slice.',
},
{
title: 'Use raw nodes last',
description: 'Reach for the full catalog only after the baseline path is readable and stable.',
},
]
}
export function getWorkflowAuthoringFlow(graphFamily: WorkflowGraphFamily): WorkflowAuthoringFlowStep[] {
const starterPathLabel = graphFamily === 'mixed' ? 'Starter Path' : STARTER_PATH_TITLES[graphFamily]
return [
{
index: 1,
title: 'Reference Path',
description:
graphFamily === 'order_line'
? 'Start from the canonical non-legacy still graph before changing individual stages.'
: 'Start from the canonical path when you want a full baseline instead of assembling from scratch.',
},
{
index: 2,
title: 'Stage Modules',
description: 'Swap or extend whole production stages with reusable graph-safe bundles.',
},
{
index: 3,
title: starterPathLabel,
description: 'Fill gaps one required step at a time and verify the minimum viable chain stays intact.',
},
{
index: 4,
title: 'Raw Node Catalog',
description: 'Use individual nodes only for advanced authoring, experiments, or edge-case overrides.',
},
]
}
export function getWorkflowAuthoringPlan(
definitions: WorkflowNodeDefinition[],
graphFamily: WorkflowGraphFamily,
activeSteps: string[],
): WorkflowAuthoringPlan {
const activeStepSet = new Set(activeSteps)
const definitionsByStep = new Map(definitions.map(definition => [definition.step, definition]))
const title = graphFamily === 'mixed' ? 'Guided Authoring' : STARTER_PATH_TITLES[graphFamily]
const description =
graphFamily === 'mixed'
? 'Keep the graph readable by preferring complete paths and stage bundles before raw node-level edits.'
: STARTER_PATH_DESCRIPTIONS[graphFamily]
const referenceBundles = getWorkflowReferenceBundles(definitions, graphFamily)
.map(bundle => ({
...bundle,
presentCount: bundle.stepIds.filter(step => activeStepSet.has(step)).length,
totalCount: bundle.stepIds.length,
}))
const moduleBundles = getWorkflowModuleBundles(definitions, graphFamily)
.map(bundle => ({
...bundle,
presentCount: bundle.stepIds.filter(step => activeStepSet.has(step)).length,
totalCount: bundle.stepIds.length,
}))
const starterDefinitions =
graphFamily === 'mixed'
? []
: STARTER_NODE_STEP_ORDER[graphFamily]
.map(step => definitionsByStep.get(step))
.filter((definition): definition is WorkflowNodeDefinition => Boolean(definition))
const starterItems = starterDefinitions.map((definition, index) => ({
index: index + 1,
definition,
isPresent: activeStepSet.has(definition.step),
}))
const starterCompletedCount = starterItems.filter(item => item.isPresent).length
const gapFillDefinitions = starterItems
.filter(item => !item.isPresent)
.map(item => item.definition)
.slice(0, 3)
const stageProgress = (() => {
if (graphFamily === 'mixed') return [] as WorkflowAuthoringStageProgress[]
const stages: WorkflowAuthoringStageProgress[] = []
const referenceBundle = referenceBundles[0]
if (referenceBundle) {
stages.push({
id: referenceBundle.id,
title: referenceBundle.label,
description: referenceBundle.description,
present: referenceBundle.presentCount,
total: referenceBundle.totalCount,
actionLabel:
referenceBundle.presentCount === 0
? `Insert ${referenceBundle.shortLabel}`
: `Reapply ${referenceBundle.shortLabel}`,
actionKind: 'reference',
bundleId: referenceBundle.id,
})
}
for (const bundle of moduleBundles) {
if (referenceBundle && haveSameStepSet(bundle.stepIds, referenceBundle.stepIds)) {
continue
}
if (bundle.presentCount === bundle.totalCount) continue
stages.push({
id: bundle.id,
title: bundle.label,
description: bundle.description,
present: bundle.presentCount,
total: bundle.totalCount,
actionLabel:
bundle.presentCount === 0
? `Insert ${bundle.shortLabel}`
: `Complete ${bundle.shortLabel}`,
actionKind: 'module',
bundleId: bundle.id,
})
}
const nextMissingStarterDefinition = starterItems.find(item => !item.isPresent)?.definition
if (nextMissingStarterDefinition) {
stages.push({
id: nextMissingStarterDefinition.step,
title: `Next missing step: ${nextMissingStarterDefinition.label}`,
description: 'Use a single-step insert only when you need to patch a gap without dropping an entire stage bundle.',
present: 0,
total: 1,
actionLabel: `Add ${nextMissingStarterDefinition.label}`,
actionKind: 'step',
step: nextMissingStarterDefinition.step,
})
}
return stages
})()
return {
title,
description,
priorities: getAuthoringPriorities(graphFamily),
authoringFlow: getWorkflowAuthoringFlow(graphFamily),
referenceBundles,
moduleBundles,
starterTitle: STARTER_PATH_TITLES[graphFamily],
starterDescription: STARTER_PATH_DESCRIPTIONS[graphFamily],
starterItems,
starterCompletedCount,
stageProgress,
gapFillDefinitions,
}
}