project workflow state -> output

This commit is contained in:
lda
2026-05-26 00:43:23 +07:00 Verified
parent fad915a186
commit 6846c649b6
16 changed files with 307 additions and 23 deletions
+2
View File
@@ -20,6 +20,7 @@ from wf_core.validation.steps import (
validate_interrupt_node,
validate_node_use,
validate_subgraph_node,
validate_workflow_output_bindings,
)
@@ -27,6 +28,7 @@ def validate_workflow(workflow: Workflow) -> ValidationReport:
report = ValidationReport()
node_defs = _collect_node_defs(workflow, report)
validate_workflow_output_bindings(workflow.output, workflow, report)
nodes_by_id = _validate_nodes(workflow, node_defs, report)
_validate_start(workflow, nodes_by_id, report)
outgoing = _validate_edges(workflow, nodes_by_id, node_defs, report)
+1
View File
@@ -18,6 +18,7 @@ class ValidationIssueCode(StrEnum):
INVALID_NODE_INPUT_FIELD = "invalid_node_input_field"
INVALID_SOURCE_PATH = "invalid_source_path"
INVALID_NODE_OUTPUT_FIELD = "invalid_node_output_field"
INVALID_WORKFLOW_OUTPUT_FIELD = "invalid_workflow_output_field"
INVALID_DESTINATION_PATH = "invalid_destination_path"
EMPTY_CONDITION_ARGS = "empty_condition_args"
INVALID_CONDITION_PATH = "invalid_condition_path"
+51
View File
@@ -16,6 +16,7 @@ from wf_core.models.steps import (
ForeachNode,
InputBinding,
InputPathBinding,
InputValueBinding,
InterruptNode,
NodeUse,
OutputBinding,
@@ -153,6 +154,56 @@ def _validate_boundary_bindings(
)
def validate_workflow_output_bindings(
bindings: list[InputBinding],
workflow: Workflow,
report: ValidationReport,
) -> None:
"""Validate final workflow output projection bindings.
Root output bindings reuse input-binding shape: graph paths flow into a
local output payload that is later validated against `output_schema`.
"""
output_fields = set(workflow.output_schema.properties)
state_root_fields = workflow.state_schema.root_fields()
input_root_fields = set(workflow.input_schema.properties)
output_targets = []
for output_index, binding in enumerate(bindings):
output_targets.append(binding.target)
destination_root = _local_root(binding.target)
if destination_root is None or (
destination_root != "." and destination_root not in output_fields
):
report.add(
ValidationIssueCode.INVALID_WORKFLOW_OUTPUT_FIELD,
f"output[{output_index}].target",
"destination field is not declared in workflow output schema",
)
if isinstance(binding, InputPathBinding) and not is_valid_source_path(
binding.path,
state_root_fields,
input_root_fields,
allow_context=True,
):
report.add(
ValidationIssueCode.INVALID_SOURCE_PATH,
f"output[{output_index}].path",
"source path must start with input., state., or context. and reference a declared root field when applicable",
)
elif not isinstance(binding, (InputPathBinding, InputValueBinding)):
report.add(
ValidationIssueCode.INVALID_WORKFLOW_OUTPUT_FIELD,
f"output[{output_index}]",
"unsupported workflow output binding",
)
if has_overlapping_paths(output_targets):
report.add(
ValidationIssueCode.INVALID_WORKFLOW_OUTPUT_FIELD,
"output",
"workflow output has overlapping output payload paths",
)
def _local_root(path: str | LocalPath) -> str | None:
try:
parts = split_local_path(path)