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 = { 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 = { cad_file: 'CAD intake assembly', order_line: 'Still-render assembly', mixed: 'Starter path', } export const STARTER_PATH_DESCRIPTIONS: Record = { 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 & { 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[] moduleBundles: WorkflowAuthoringBundleStatus[] 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, } }