fix: validate workflow output target references
This commit is contained in:
@@ -170,6 +170,9 @@ implicit same-name state fallback. Use the map tool only for compatibility:
|
||||
`set_workflow_output_map` cannot preserve canonical path/value order, literals,
|
||||
or repeated-source fan-out.
|
||||
|
||||
The equivalent JSON-RPC operation is
|
||||
`workflow.draft_workspaces.set_workflow_output_bindings`.
|
||||
|
||||
Advanced workspace tools:
|
||||
|
||||
- `wf.workflow.list_draft_workspaces`: find mutable draft sessions.
|
||||
|
||||
@@ -317,6 +317,9 @@ For the copy-less wrapper authoring path, use:
|
||||
`wf.workflow.set_workflow_output_bindings`, their compatibility map adapters,
|
||||
and `wf.workflow.set_draft_route` to fix low-confidence hints or explicit
|
||||
`missing_decisions`.
|
||||
JSON-RPC clients use
|
||||
`workflow.draft_workspaces.set_workflow_output_bindings` for the same
|
||||
canonical replacement.
|
||||
4. `wf.workflow.validate_draft_workspace` to refresh diagnostics.
|
||||
5. `wf.workflow.create_wrapper_from_workspace` to save the wrapper artifact.
|
||||
6. `wf.workflow.call_capability` with `workflow.<artifact_id>.v<version>` to
|
||||
|
||||
@@ -86,10 +86,10 @@ def _draft_schema(draft: Mapping[str, Any], key: str) -> dict[str, Any]:
|
||||
return deepcopy(value)
|
||||
|
||||
|
||||
def _overlapping_input_targets_error(
|
||||
def _overlapping_input_binding_targets_error(
|
||||
bindings: Sequence[InputBinding],
|
||||
) -> ValueError:
|
||||
"""Describe the first overlapping binding pair with stable input indexes."""
|
||||
"""Describe the first overlapping input-shaped target pair."""
|
||||
for left_index, left in enumerate(bindings):
|
||||
for right_index in range(left_index + 1, len(bindings)):
|
||||
right = bindings[right_index]
|
||||
@@ -137,22 +137,6 @@ def _workflow_source_schema(
|
||||
return value
|
||||
|
||||
|
||||
def _overlapping_workflow_output_targets_error(
|
||||
bindings: Sequence[InputBinding],
|
||||
) -> ValueError:
|
||||
"""Describe the first overlapping public-output target pair."""
|
||||
for left_index, left in enumerate(bindings):
|
||||
for right_index in range(left_index + 1, len(bindings)):
|
||||
right = bindings[right_index]
|
||||
if paths_overlap(left.target, right.target):
|
||||
return ValueError(
|
||||
f"bindings[{left_index}].target {str(left.target)!r} "
|
||||
f"overlaps bindings[{right_index}].target "
|
||||
f"{str(right.target)!r}"
|
||||
)
|
||||
raise AssertionError("overlap error requested without overlapping targets")
|
||||
|
||||
|
||||
def _step_input_bindings_patch(
|
||||
*,
|
||||
workspace: WorkflowDraftWorkspace,
|
||||
@@ -459,7 +443,7 @@ class WorkflowDraftAuthoringApi:
|
||||
|
||||
targets = [binding.target for binding in bindings]
|
||||
if has_overlapping_paths(targets):
|
||||
raise _overlapping_input_targets_error(bindings)
|
||||
raise _overlapping_input_binding_targets_error(bindings)
|
||||
|
||||
projected_input = _draft_schema(workspace.draft, "input_schema")
|
||||
projected_state = _draft_schema(workspace.draft, "state_schema")
|
||||
@@ -572,7 +556,7 @@ class WorkflowDraftAuthoringApi:
|
||||
) from exc
|
||||
|
||||
if has_overlapping_paths(binding.target for binding in bindings):
|
||||
raise _overlapping_workflow_output_targets_error(bindings)
|
||||
raise _overlapping_input_binding_targets_error(bindings)
|
||||
|
||||
projected = output_schema
|
||||
for index, binding in enumerate(bindings):
|
||||
|
||||
@@ -129,6 +129,14 @@ def project_schema_path_to_schema_path(
|
||||
)
|
||||
leaf = target_parts[-1]
|
||||
if leaf in properties:
|
||||
# Validate an existing target reference against the target document
|
||||
# before source definitions are merged. Otherwise an unresolved target
|
||||
# could be silently repaired by an equivalent source-side definition.
|
||||
_resolve_local_reference(
|
||||
projected,
|
||||
properties[leaf],
|
||||
label=".".join(target_parts),
|
||||
)
|
||||
if allow_existing_equivalent and properties[leaf] == source_value:
|
||||
_merge_definition_block(projected, source_schema, "$defs")
|
||||
_merge_definition_block(projected, source_schema, "definitions")
|
||||
|
||||
@@ -481,6 +481,27 @@ def test_schema_path_exists_follows_local_defs() -> None:
|
||||
assert schema_path_exists(schema, ("report", "missing")) is False
|
||||
|
||||
|
||||
def test_project_schema_path_rejects_unresolved_equivalent_target_reference() -> None:
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"unresolved reference '#/\$defs/Report'",
|
||||
):
|
||||
project_schema_path_to_schema_path(
|
||||
target_schema={
|
||||
"type": "object",
|
||||
"properties": {"report": {"$ref": "#/$defs/Report"}},
|
||||
},
|
||||
source_schema={
|
||||
"type": "object",
|
||||
"properties": {"report": {"$ref": "#/$defs/Report"}},
|
||||
"$defs": {"Report": {"type": "object", "properties": {}}},
|
||||
},
|
||||
source_parts=("report",),
|
||||
target_parts=("report",),
|
||||
allow_existing_equivalent=True,
|
||||
)
|
||||
|
||||
|
||||
def test_project_schema_path_rejects_missing_nested_source() -> None:
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
|
||||
Reference in New Issue
Block a user