docs: explain safe compatibility merges

This commit is contained in:
lda
2026-07-29 08:34:30 +07:00 Verified
parent 1cee713850
commit 69ac8e2406
5 changed files with 38 additions and 21 deletions
+4 -3
View File
@@ -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
View File
@@ -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,
+18 -10
View File
@@ -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.
+4 -2
View File
@@ -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
+2 -1
View File
@@ -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"
]