docs: explain safe compatibility merges
This commit is contained in:
@@ -19,9 +19,10 @@
|
||||
- [x] Focused draft authoring can add or replace literal node-input bindings
|
||||
comparable to `WorkflowBuilder.use(input=[{"target": ..., "value": ...}])`
|
||||
through canonical API, RPC, MCP, and CLI surfaces.
|
||||
- [ ] Compatibility step input/output maps can still collapse valid canonical
|
||||
fan-out bindings. Canonical input and output replacement preserve ordered
|
||||
fan-out, but later compatibility-map merges remain inherently lossy.
|
||||
- [x] Compatibility step input/output map merges reject canonical lists they
|
||||
cannot reproduce exactly, and workflow-output map merges reject requested
|
||||
sources with ambiguous fan-out. Canonical replacement remains the supported
|
||||
path for ordered fan-out and mixed path/literal bindings.
|
||||
- [x] Focused workflow-output authoring supports literal output bindings through
|
||||
canonical Python, JSON-RPC, MCP, and CLI replacement surfaces.
|
||||
- [x] Workflow-output replacement projects nested `input.*` and `state.*`
|
||||
|
||||
+10
-5
@@ -460,11 +460,9 @@ class WorkflowDraftApi:
|
||||
if isinstance(workspace, dict):
|
||||
return workspace
|
||||
step = _draft_step(workspace.draft, step_id)
|
||||
existing_map, input_values = (
|
||||
_require_lossless_step_input_map_round_trip(
|
||||
step.get("input", []),
|
||||
step_id=step_id,
|
||||
)
|
||||
existing_map, input_values = _require_lossless_step_input_map_round_trip(
|
||||
step.get("input", []),
|
||||
step_id=step_id,
|
||||
)
|
||||
input_map = {**existing_map, **input_map}
|
||||
return await self.patch_draft_workspace(
|
||||
@@ -665,6 +663,13 @@ class WorkflowDraftApi:
|
||||
projected = updated
|
||||
return projected if changed else output_schema
|
||||
|
||||
def _step_output_map(self, *, workspace_id: str, step_id: str) -> dict[str, str]:
|
||||
"""Read one step's outputs for legacy focused binding helpers."""
|
||||
workspace = self._draft_store().get_workspace(workspace_id)
|
||||
step = _draft_step(workspace.draft, step_id)
|
||||
return _output_map_from_payload(step.get("output", []))
|
||||
|
||||
|
||||
def _workflow_source_schema(
|
||||
draft: Mapping[str, Any],
|
||||
source_path: str,
|
||||
|
||||
@@ -418,8 +418,9 @@ def set_step_input(
|
||||
typer.Option(
|
||||
"--merge",
|
||||
help=(
|
||||
"Compatibility map-only mode: preserve existing bindings and "
|
||||
"add/update --map entries."
|
||||
"Compatibility map-only mode: preserve existing map-shaped "
|
||||
"bindings and add/update --map entries; rejects existing "
|
||||
"bindings that cannot round-trip safely."
|
||||
),
|
||||
),
|
||||
] = False,
|
||||
@@ -428,7 +429,10 @@ def set_step_input(
|
||||
|
||||
By default, repeated --map and --value flags replace the complete ordered
|
||||
binding list. --bindings-file replaces from canonical JSON, while --clear
|
||||
sends an empty list. Use --merge only with map-only compatibility edits.
|
||||
sends an empty list. Use --merge only with map-only compatibility edits;
|
||||
it rejects existing bindings that cannot round-trip safely. Canonical
|
||||
fan-out or reordered path/literal bindings require complete canonical
|
||||
replacement.
|
||||
|
||||
Targets are rootless node-local paths. For example, use
|
||||
`--map input.title=report.title`, not
|
||||
@@ -527,8 +531,9 @@ def set_step_output_map(
|
||||
typer.Option(
|
||||
"--merge",
|
||||
help=(
|
||||
"Compatibility-only and potentially lossy: preserve existing "
|
||||
"bindings and add/update --map entries."
|
||||
"Compatibility-only: preserve existing map-shaped bindings and "
|
||||
"add/update --map entries; fan-out must replace the complete "
|
||||
"canonical binding list."
|
||||
),
|
||||
),
|
||||
] = False,
|
||||
@@ -538,8 +543,9 @@ def set_step_output_map(
|
||||
By default, ``--map LOCAL_SOURCE=STATE_TARGET`` replaces the complete
|
||||
ordered canonical binding list. ``--bindings-file`` accepts an ordered
|
||||
canonical JSON array, and ``--clear`` replaces with no bindings. Use
|
||||
``--merge`` only with ``--map`` for compatibility-only and potentially
|
||||
lossy map edits.
|
||||
``--merge`` only with ``--map`` for compatibility-only edits; existing
|
||||
fan-out is rejected because it cannot round-trip through the map
|
||||
representation. Fan-out must replace the complete canonical binding list.
|
||||
|
||||
Run `wf draft validate <workspace_id>` after map edits; validation reports
|
||||
unresolved paths and conflicting writes.
|
||||
@@ -649,8 +655,8 @@ def set_workflow_output(
|
||||
typer.Option(
|
||||
"--merge",
|
||||
help=(
|
||||
"Compatibility-only and potentially lossy: preserve existing "
|
||||
"bindings and add/update map-only entries."
|
||||
"Compatibility-only: preserve existing bindings and add/update "
|
||||
"map-only entries; rejects ambiguous fan-out sources."
|
||||
),
|
||||
),
|
||||
] = False,
|
||||
@@ -662,7 +668,9 @@ def set_workflow_output(
|
||||
binding list. ``--bindings-file`` accepts an ordered canonical JSON array
|
||||
and preserves exact path/value interleaving. ``--clear`` restores the
|
||||
implicit same-name state fallback declared by ``output_schema``. Use
|
||||
--merge only with --map for compatibility-only and potentially lossy edits.
|
||||
--merge only with --map for compatibility-only edits. A requested source
|
||||
with several existing fan-out bindings requires complete canonical
|
||||
replacement because ambiguous fan-out sources cannot identify one binding.
|
||||
|
||||
This edits WorkflowDraft.output (top-level workflow output). Use
|
||||
wf draft set-output for step-level output bindings.
|
||||
|
||||
@@ -757,6 +757,8 @@ def test_wf_draft_map_help_explains_replace_merge_and_validate() -> None:
|
||||
workflow_output_help = " ".join(workflow_output_result.output.split())
|
||||
assert "replace the complete ordered binding list" in input_help
|
||||
assert "Use --merge only" in input_help
|
||||
assert "rejects existing bindings" in input_help
|
||||
assert "cannot round-trip safely" in input_help
|
||||
assert "draft validate" in input_help
|
||||
assert "input.text=text" in input_help
|
||||
assert "input.text=local.text" in input_help
|
||||
@@ -764,7 +766,7 @@ def test_wf_draft_map_help_explains_replace_merge_and_validate() -> None:
|
||||
assert "LOCAL_SOURCE=STATE_TARGET" in output_help
|
||||
assert "ordered canonical JSON array" in output_help
|
||||
assert "replace with no bindings" in output_help.lower()
|
||||
assert "compatibility-only and potentially lossy" in output_help
|
||||
assert "replace the complete canonical binding list" in output_help
|
||||
assert "draft validate" in output_help
|
||||
assert "complete ordered workflow output binding list" in workflow_output_help
|
||||
assert "GRAPH_SOURCE=OUTPUT_TARGET" in workflow_output_help
|
||||
@@ -774,7 +776,7 @@ def test_wf_draft_map_help_explains_replace_merge_and_validate() -> None:
|
||||
assert "inspect --include-draft" in workflow_output_help
|
||||
assert "draft.output" in workflow_output_help
|
||||
assert "Use --merge only" in workflow_output_help
|
||||
assert "compatibility-only and potentially lossy" in workflow_output_help
|
||||
assert "ambiguous fan-out sources" in workflow_output_help
|
||||
assert "draft validate" in workflow_output_help
|
||||
|
||||
|
||||
|
||||
@@ -1725,7 +1725,8 @@ def test_wf_draft_set_output_replaces_canonical_bindings_over_rpc(
|
||||
],
|
||||
)
|
||||
|
||||
assert merged.exit_code == 0, merged.output
|
||||
assert merged.exit_code == 1
|
||||
assert "complete canonical binding list" in merged.output
|
||||
assert [method for method, _params in rpc_calls] == [
|
||||
"workflow.draft_workspaces.set_step_output_map"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user