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
@@ -1,16 +1,17 @@
import { useEffect, type ReactNode } from 'react'
import { useEffect, useRef, type ReactNode } from 'react'
import { Boxes, Milestone, X } from 'lucide-react'
import type { WorkflowNodeDefinition } from '../../api/workflows'
import type { WorkflowGraphFamily } from './workflowNodeLibrary'
import { WorkflowAuthoringSectionContent } from './WorkflowAuthoringSectionContent'
import { WorkflowAuthoringSectionSelector } from './WorkflowAuthoringSectionSelector'
import {
type WorkflowAuthoringActions,
type WorkflowAuthoringPosition,
} from './workflowAuthoringActions'
import { useWorkflowAuthoringSurface } from './workflowAuthoringSurface'
export const NODE_COMMAND_MENU_WIDTH = 360
export const NODE_COMMAND_MENU_WIDTH = 380
interface NodeCommandMenuProps {
definitions: WorkflowNodeDefinition[]
@@ -31,7 +32,8 @@ export function NodeCommandMenu({
onClose,
renderIcon,
}: NodeCommandMenuProps) {
const { activeSection, insertBindings, plan, sections, setActiveSection } =
const containerRef = useRef<HTMLDivElement | null>(null)
const { activeSection, activeSectionDetail, chrome, insertBindings, plan, sections, setActiveSection } =
useWorkflowAuthoringSurface({
definitions,
graphFamily,
@@ -52,19 +54,28 @@ export function NodeCommandMenu({
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onClose])
useEffect(() => {
const handlePointerDown = (event: PointerEvent) => {
if (!containerRef.current) return
if (containerRef.current.contains(event.target as Node)) return
onClose()
}
window.addEventListener('pointerdown', handlePointerDown)
return () => window.removeEventListener('pointerdown', handlePointerDown)
}, [onClose])
return (
<div
className="flex max-h-[calc(100vh-2rem)] flex-col overflow-hidden rounded-2xl border border-border-default bg-surface shadow-2xl"
ref={containerRef}
className="flex max-h-[calc(100vh-1.5rem)] flex-col overflow-hidden rounded-2xl border border-border-default bg-surface shadow-2xl"
style={{ width: NODE_COMMAND_MENU_WIDTH }}
>
<div className="border-b border-border-default px-4 py-3">
<div className="border-b border-border-default px-3 py-2.5">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold text-content">Workflow Authoring</p>
<p className="text-xs text-content-muted">
Use complete reference paths, modules, starter steps, or the raw node library directly on the canvas.
</p>
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-content-muted">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-1.5">
<p className="text-sm font-semibold text-content">{chrome.menuTitle}</p>
<span className="rounded-full border border-border-default bg-surface-hover px-2 py-0.5">
{activeSteps.length} on canvas
</span>
@@ -80,7 +91,13 @@ export function NodeCommandMenu({
{plan.moduleBundles.length} modules
</span>
)}
<span className="rounded-full border border-border-default bg-surface-hover px-2 py-0.5">
Esc closes
</span>
</div>
<p className="mt-1 text-xs text-content-muted">
{chrome.menuHelper}
</p>
</div>
<button
type="button"
@@ -93,29 +110,28 @@ export function NodeCommandMenu({
</div>
</div>
<div className="flex-1 overflow-y-auto px-3 py-3">
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{sections.map(section => {
const Icon = section.icon
const isActive = activeSection === section.key
return (
<button
key={section.key}
type="button"
onClick={() => setActiveSection(section.key)}
className={`inline-flex items-center gap-1 rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${
isActive
? 'bg-accent text-white'
: 'border border-border-default bg-surface text-content-secondary hover:bg-surface-hover'
}`}
title={section.helper}
>
<Icon size={12} />
{section.label}
</button>
)
})}
<div className="flex-1 overflow-y-auto px-3 py-2.5">
<div className="space-y-2.5">
<div className="sticky top-0 z-10 -mx-1 rounded-xl bg-surface/95 px-1 pb-2 pt-1 backdrop-blur">
<div className="mb-2 flex items-center justify-between gap-2 rounded-xl border border-border-default bg-surface-hover/30 px-3 py-2">
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-wide text-content-secondary">
{chrome.guideEyebrow}
</p>
<p className="text-xs text-content-muted">
{activeSectionDetail?.helper ?? chrome.guideHelper}
</p>
</div>
<span className="shrink-0 rounded-full border border-border-default bg-surface px-2 py-0.5 text-[11px] text-content-muted">
{activeSectionDetail?.label ?? activeSectionDetail?.tone ?? 'Guided'}
</span>
</div>
<WorkflowAuthoringSectionSelector
sections={sections}
activeSection={activeSection}
onSelectSection={setActiveSection}
variant="pill"
/>
</div>
<WorkflowAuthoringSectionContent
@@ -126,7 +142,7 @@ export function NodeCommandMenu({
actions={insertBindings}
renderIcon={renderIcon}
nodesVariant="menu"
searchPlaceholder="Search nodes"
searchPlaceholder="Search raw nodes"
autoFocusSearch
/>
</div>