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
@@ -3,7 +3,16 @@ import pytest
from app.core.process_steps import StepName
from app.domains.rendering.models import OutputType, WorkflowDefinition, WorkflowRun
from app.domains.rendering.workflow_config_utils import build_preset_workflow_config
from app.domains.rendering.workflow_graph_runtime import _STILL_TASK_KEYS, _TURNTABLE_TASK_KEYS
from app.domains.rendering.workflow_graph_runtime import (
_AUTO_POPULATE_RUNTIME_KEYS,
_GLB_BBOX_RUNTIME_KEYS,
_MATERIAL_MAP_RUNTIME_KEYS,
_NOTIFY_RUNTIME_KEYS,
_OUTPUT_SAVE_RUNTIME_KEYS,
_RESOLVE_TEMPLATE_RUNTIME_KEYS,
_STILL_TASK_KEYS,
_TURNTABLE_TASK_KEYS,
)
from app.domains.rendering.workflow_node_registry import (
get_node_definition,
list_node_definitions,
@@ -121,6 +130,29 @@ def test_graph_render_node_fields_are_supported_by_runtime_dispatch():
assert turntable_runtime_fields <= _TURNTABLE_TASK_KEYS
def test_bridge_node_fields_match_runtime_dispatch_contracts():
resolve_template = get_node_definition(StepName.RESOLVE_TEMPLATE)
material_map = get_node_definition(StepName.MATERIAL_MAP_RESOLVE)
auto_populate = get_node_definition(StepName.AUTO_POPULATE_MATERIALS)
glb_bbox = get_node_definition(StepName.GLB_BBOX)
output_save = get_node_definition(StepName.OUTPUT_SAVE)
notify = get_node_definition(StepName.NOTIFY)
assert resolve_template is not None
assert material_map is not None
assert auto_populate is not None
assert glb_bbox is not None
assert output_save is not None
assert notify is not None
assert {field.key for field in resolve_template.fields} == _RESOLVE_TEMPLATE_RUNTIME_KEYS
assert {field.key for field in material_map.fields} == _MATERIAL_MAP_RUNTIME_KEYS
assert {field.key for field in auto_populate.fields} == _AUTO_POPULATE_RUNTIME_KEYS
assert {field.key for field in glb_bbox.fields} == _GLB_BBOX_RUNTIME_KEYS
assert {field.key for field in output_save.fields} == _OUTPUT_SAVE_RUNTIME_KEYS
assert {field.key for field in notify.fields} == _NOTIFY_RUNTIME_KEYS
def test_order_line_setup_and_template_contracts_expose_runtime_outputs():
setup = get_node_definition(StepName.ORDER_LINE_SETUP)
template = get_node_definition(StepName.RESOLVE_TEMPLATE)
@@ -178,7 +210,13 @@ def test_order_line_setup_and_template_contracts_expose_runtime_outputs():
"include_populated_products",
}
assert output.input_contract["requires"] == ["order_line_context"]
assert output.input_contract["requires_any"] == ["rendered_image", "rendered_frames", "rendered_video"]
assert output.input_contract["requires_any"] == [
"rendered_image",
"rendered_frames",
"rendered_video",
"blend_asset",
]
assert "blend_asset" in output.artifact_roles_consumed
assert set(output.output_contract["provides"]) >= {"media_asset", "workflow_result"}
assert {field.key for field in output.fields} == {
"expected_artifact_role",
@@ -193,6 +231,7 @@ def test_order_line_setup_and_template_contracts_expose_runtime_outputs():
"rendered_frames",
"rendered_video",
"workflow_result",
"blend_asset",
]
assert {field.key for field in notify.fields} == {"channel", "require_armed_render"}