Files
dify/api/services/dify_builder/preflight.py
T
Charles YaoandClaude Opus 5 fcb7c719ef feat(dify-builder): preflight the draft in apply_repair before writing it
ESQ1-302 and ESQ1-303 both died at Graph.init -- before workflow_started,
so the Builder saw an error frame with no run id and no rows, and spent
runs and approvals discovering what a validation pass could have said
up front. apply_repair now runs that pass (preflight_errors: the same
node-data validation create_node performs, with no factory or runtime
state) over the graph it is about to write and raises PreflightError,
naming each node Graph.init would reject.

Only NEW problems count: a pre-existing broken node the repair does not
touch must not veto an unrelated fix, and a repair that heals it passes.
PreflightError is a ValueError so the handlers' existing stale-intent
guards already surface it as a card.

The real ESQ1-303 dev draft is the fixture: preflight names node2 and
its numeric value, and passes once the value is a string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:21:17 -07:00

41 lines
1.8 KiB
Python

"""Dry ``Graph.init`` for a draft graph: the node-data validation the engine
performs before it starts a run, without the run.
Both ESQ1-302 (an http-request body item without ``type``) and ESQ1-303 (an
if-else ``value`` written as a JSON number) died at ``Graph.init`` -- before
``workflow_started``, so the Builder saw an error frame with no run id and no
rows, and spent runs and approvals discovering what a validation pass could
have said up front. ``preflight_errors`` runs that pass over a graph dict.
Pure with respect to the Builder: no session, no DB, no engine state. It
imports ``core.workflow.node_factory`` (the same validation ``create_node``
does), which is why it lives in ``services/dify_builder`` and not in the
I/O-free ``core/dify_builder``.
"""
from collections.abc import Mapping
from core.dify_builder.models import Graph
from core.workflow.node_factory import validate_node_config
# graphon's ``Graph._filter_canvas_only_nodes`` drops persisted note widgets
# (top-level ``type == "custom-note"``, empty ``data.type``) before validating
# node configs. Mirror it, or every draft with a note would fail preflight.
_CANVAS_ONLY_NODE_TYPE = "custom-note"
def preflight_errors(graph: Graph) -> list[str]:
"""Every node of ``graph`` that ``Graph.init`` would reject, one message
each, in node order. Each message starts with ``node <id> (<type>):``
(see ``core.workflow.node_factory.validate_node_config``). Empty when the
draft would start."""
errors: list[str] = []
for node in graph.get("nodes") or []:
if not isinstance(node, Mapping) or node.get("type") == _CANVAS_ONLY_NODE_TYPE:
continue
try:
validate_node_config(node)
except ValueError as exc:
errors.append(str(exc))
return errors