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:
@@ -105,7 +105,6 @@ _ORDER_LINE_SETUP_REQUIRED_STEPS = {
|
||||
StepName.RESOLVE_TEMPLATE,
|
||||
StepName.MATERIAL_MAP_RESOLVE,
|
||||
StepName.AUTO_POPULATE_MATERIALS,
|
||||
StepName.GLB_BBOX,
|
||||
StepName.BLENDER_STILL,
|
||||
StepName.BLENDER_TURNTABLE,
|
||||
StepName.OUTPUT_SAVE,
|
||||
@@ -322,6 +321,23 @@ def _format_order_line_context_label(order: Order, line: OrderLine) -> tuple[str
|
||||
)
|
||||
|
||||
|
||||
def _get_order_line_context_renderability(
|
||||
order: Order,
|
||||
line: OrderLine,
|
||||
*,
|
||||
allow_completed_order_rerender: bool = False,
|
||||
) -> tuple[bool, str | None]:
|
||||
if line.render_status == "cancelled":
|
||||
return False, "line_cancelled"
|
||||
if order.status == OrderStatus.rejected:
|
||||
return False, "order_closed"
|
||||
if order.status == OrderStatus.completed and not allow_completed_order_rerender:
|
||||
return False, "order_closed"
|
||||
if line.product is None or line.product.cad_file_id is None:
|
||||
return False, "missing_cad_file"
|
||||
return True, None
|
||||
|
||||
|
||||
def _issue(
|
||||
severity: str,
|
||||
code: str,
|
||||
@@ -350,6 +366,16 @@ def _node_status(issues: list[WorkflowPreflightIssueOut], *, supported: bool) ->
|
||||
|
||||
|
||||
def _infer_expected_context_kind(ordered_nodes: list) -> str:
|
||||
concrete_families = {
|
||||
definition.family
|
||||
for node in ordered_nodes
|
||||
if (definition := get_node_definition(node.step)) is not None
|
||||
and definition.family in {"cad_file", "order_line"}
|
||||
}
|
||||
if concrete_families == {"order_line"}:
|
||||
return "order_line"
|
||||
if concrete_families == {"cad_file"}:
|
||||
return "cad_file"
|
||||
if any(node.step in _ORDER_LINE_RUNTIME_STEPS for node in ordered_nodes):
|
||||
return "order_line"
|
||||
return "cad_file"
|
||||
@@ -444,7 +470,12 @@ def _build_workflow_preflight_for_config(
|
||||
)
|
||||
|
||||
if context_kind == "order_line" and order_line is not None:
|
||||
setup = prepare_order_line_render_context(session, str(order_line.id), persist_state=False)
|
||||
setup = prepare_order_line_render_context(
|
||||
session,
|
||||
str(order_line.id),
|
||||
persist_state=False,
|
||||
allow_completed_order_rerender=True,
|
||||
)
|
||||
if setup.order_line is not None:
|
||||
resolved_order_line_id = setup.order_line.id
|
||||
if setup.cad_file is not None:
|
||||
@@ -567,6 +598,35 @@ def _build_workflow_preflight_for_config(
|
||||
)
|
||||
)
|
||||
|
||||
if node.step == StepName.GLB_BBOX and context_kind == "order_line":
|
||||
setup_indices = [
|
||||
node_indices[candidate.id]
|
||||
for candidate in workflow_context.ordered_nodes
|
||||
if candidate.step == StepName.ORDER_LINE_SETUP
|
||||
]
|
||||
has_prior_setup = any(index < node_indices[node.id] for index in setup_indices)
|
||||
if not has_prior_setup:
|
||||
node_issues.append(
|
||||
_issue(
|
||||
"error",
|
||||
"missing_order_line_setup",
|
||||
"This node requires an earlier order_line_setup node when used with an order-line context.",
|
||||
node_id=node.id,
|
||||
step=node.step.value,
|
||||
)
|
||||
)
|
||||
elif setup is None or not setup.is_ready:
|
||||
reason = setup.reason if setup is not None else "order_line_setup_not_executed"
|
||||
node_issues.append(
|
||||
_issue(
|
||||
"error",
|
||||
"setup_not_ready",
|
||||
f"Order-line setup is not ready for this node: {reason}.",
|
||||
node_id=node.id,
|
||||
step=node.step.value,
|
||||
)
|
||||
)
|
||||
|
||||
if node.step in _RESOLVE_TEMPLATE_RECOMMENDED_STEPS:
|
||||
template_indices = [
|
||||
node_indices[candidate.id]
|
||||
@@ -763,15 +823,23 @@ async def list_workflow_order_line_contexts(
|
||||
options: list[WorkflowOrderLineContextOptionOut] = []
|
||||
for line in order.lines:
|
||||
label, meta = _format_order_line_context_label(order, line)
|
||||
is_renderable, renderability_reason = _get_order_line_context_renderability(
|
||||
order,
|
||||
line,
|
||||
allow_completed_order_rerender=True,
|
||||
)
|
||||
options.append(
|
||||
WorkflowOrderLineContextOptionOut(
|
||||
value=line.id,
|
||||
label=label,
|
||||
meta=meta,
|
||||
is_renderable=is_renderable,
|
||||
renderability_reason=renderability_reason,
|
||||
)
|
||||
)
|
||||
|
||||
if options:
|
||||
options.sort(key=lambda option: (not option.is_renderable, option.label.lower()))
|
||||
groups.append(
|
||||
WorkflowOrderLineContextGroupOut(
|
||||
order_id=order.id,
|
||||
@@ -780,6 +848,12 @@ async def list_workflow_order_line_contexts(
|
||||
)
|
||||
)
|
||||
|
||||
groups.sort(
|
||||
key=lambda group: (
|
||||
not any(option.is_renderable for option in group.options),
|
||||
group.order_label.lower(),
|
||||
)
|
||||
)
|
||||
return groups
|
||||
|
||||
|
||||
@@ -948,7 +1022,10 @@ async def _dispatch_workflow_for_config(
|
||||
workflow_config: dict,
|
||||
context_id: str,
|
||||
) -> WorkflowDispatchResponse:
|
||||
from app.domains.rendering.workflow_executor import prepare_workflow_context
|
||||
from app.domains.rendering.workflow_executor import (
|
||||
prepare_workflow_context,
|
||||
submit_prepared_workflow_tasks,
|
||||
)
|
||||
from app.domains.rendering.workflow_graph_runtime import execute_graph_workflow
|
||||
from app.domains.rendering.workflow_run_service import (
|
||||
create_workflow_run,
|
||||
|
||||
Reference in New Issue
Block a user