fix: preserve validation errors and subgraph pins

This commit is contained in:
lda
2026-08-31 00:32:52 +07:00 Verified
parent 15d343f637
commit 40b0f28b71
5 changed files with 115 additions and 7 deletions
+2 -1
View File
@@ -15,6 +15,7 @@ from wf_artifacts import (
ArtifactKind,
RequiredCapability,
WorkflowArtifact,
WorkflowPlanValidationError,
artifact_catalog_entry,
)
from wf_artifacts import (
@@ -263,7 +264,7 @@ class WorkflowArtifactApi:
return _PROJECT_VALIDATE_ARTIFACT(
_invalid_artifact_plan_payload(_diagnostic_from_validation_error(exc))
)
except ValueError as exc:
except WorkflowPlanValidationError as exc:
return _PROJECT_VALIDATE_ARTIFACT(
_invalid_artifact_plan_payload(
{
+2 -1
View File
@@ -21,7 +21,7 @@ from .drafts import (
patch_workflow_draft,
validate_workflow_draft,
)
from .factory import create_workflow_artifact_from_plan
from .factory import WorkflowPlanValidationError, create_workflow_artifact_from_plan
from .models import (
ArtifactKind,
AvailableCapability,
@@ -78,6 +78,7 @@ __all__ = [
"WorkflowArtifact",
"WorkflowArtifactCatalogEntry",
"WorkflowArtifactStore",
"WorkflowPlanValidationError",
"WorkflowCapabilityRef",
"WorkflowDeployment",
"WorkflowDraftWorkspace",
+29 -5
View File
@@ -3,12 +3,17 @@ from __future__ import annotations
from collections.abc import Mapping
from wf_core import ReducerRef, Workflow
from wf_core.models.workflow_refs import WorkflowRef
from wf_platform import CapabilityRef, NodeSpecInventory, hash_json_schema
from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact
from .references import normalize_plan_node_refs
class WorkflowPlanValidationError(ValueError):
"""Expected structural validation failure while preparing a workflow plan."""
def create_workflow_artifact_from_plan(
*,
artifact_id: str,
@@ -47,6 +52,7 @@ def create_workflow_artifact_from_plan(
outcomes=outcomes,
plan=normalized_plan,
required_capabilities=list(required.values()),
workflow_dependencies=_workflow_dependencies_from_plan(normalized_plan),
created_from_catalog_version=created_from_catalog_version,
)
@@ -54,7 +60,9 @@ def create_workflow_artifact_from_plan(
def _required_object_field(plan: JsonObject, field_name: str) -> JsonObject:
value = plan.get(field_name)
if not isinstance(value, dict):
raise ValueError(f"workflow plan is missing object field {field_name!r}")
raise WorkflowPlanValidationError(
f"workflow plan is missing object field {field_name!r}"
)
return value
@@ -62,11 +70,11 @@ def _validate_workflow_plan(plan: JsonObject) -> None:
try:
workflow = Workflow.model_validate(plan)
except Exception as exc:
raise ValueError(f"invalid workflow plan: {exc}") from exc
raise WorkflowPlanValidationError(f"invalid workflow plan: {exc}") from exc
node_ids = {node.id for node in workflow.nodes}
if workflow.start not in node_ids:
raise ValueError(
raise WorkflowPlanValidationError(
f"invalid workflow plan: start node {workflow.start!r} does not exist"
)
@@ -76,15 +84,31 @@ def _validate_workflow_plan(plan: JsonObject) -> None:
edge_sources = set(node_ids)
for edge in workflow.edges:
if edge.from_ not in edge_sources:
raise ValueError(
raise WorkflowPlanValidationError(
f"invalid workflow plan: edge source {edge.from_!r} does not exist"
)
if edge.to not in node_ids and edge.to != "__end__":
raise ValueError(
raise WorkflowPlanValidationError(
f"invalid workflow plan: edge destination {edge.to!r} does not exist"
)
def _workflow_dependencies_from_plan(plan: JsonObject) -> dict[str, int]:
"""Collect immutable artifact pins from native saved-subgraph steps."""
nodes = plan.get("nodes")
if not isinstance(nodes, list):
return {}
dependencies: dict[str, int] = {}
for node in nodes:
if not isinstance(node, dict) or node.get("type") != "subgraph":
continue
workflow_ref = WorkflowRef.model_validate(node.get("workflow"))
if workflow_ref.artifact_id is not None and workflow_ref.version is not None:
dependencies[workflow_ref.artifact_id] = workflow_ref.version
return dependencies
def _required_reducers_from_plan(plan: JsonObject) -> dict[str, RequiredCapability]:
"""Infer reducer dependencies from declared state fields in one plan."""
state_schema = plan.get("state_schema")