docs: complete nested local draft bindings

This commit is contained in:
lda
2026-07-22 09:15:48 +07:00 Verified
parent cde0a3c14f
commit ae4b21fce4
8 changed files with 90 additions and 33 deletions
+3 -3
View File
@@ -8,10 +8,10 @@
## Draft data-shaping parity ## Draft data-shaping parity
- [ ] `wf draft bind` rejects nested node-local targets such as - [x] `wf draft bind` rejects nested node-local targets such as
`local.report.title`, even though canonical `LocalPath`, the runtime binding `local.report.title`, even though canonical `LocalPath`, the runtime binding
resolver, and `WorkflowBuilder` support nested local paths. resolver, and `WorkflowBuilder` support nested local paths.
- [ ] Capability-step authoring persists nested input targets but silently skips - [x] Capability-step authoring persists nested input targets but silently skips
workflow input/state schema projection when a target has more than one path workflow input/state schema projection when a target has more than one path
segment. segment.
- [ ] No atomic API/RPC/CLI helper assembles one structured node input from - [ ] No atomic API/RPC/CLI helper assembles one structured node input from
@@ -29,7 +29,7 @@
- [ ] Workflow output schema projection skips nested sources such as - [ ] Workflow output schema projection skips nested sources such as
`state.report.title`, leaving callers to patch the output schema manually even `state.report.title`, leaving callers to patch the output schema manually even
when the nested source schema is already declared. when the nested source schema is already declared.
- [ ] CLI help and agent instructions describe step-input targets as bare local - [x] CLI help and agent instructions describe step-input targets as bare local
fields and do not document the nested composition behavior already supported fields and do not document the nested composition behavior already supported
by the canonical runtime model. by the canonical runtime model.
+5
View File
@@ -549,6 +549,11 @@ clear operator feedback before adding more architecture.
`revision_conflict` while the patch path retains its mutation-time race `revision_conflict` while the patch path retains its mutation-time race
guard. Implementation: guard. Implementation:
[`draft semantic revision precedence`](historical/superpowers/plans/2026-07-22-draft-semantic-revision-precedence.md). [`draft semantic revision precedence`](historical/superpowers/plans/2026-07-22-draft-semantic-revision-precedence.md).
- Completed: focused draft bind and capability-step insertion now preserve
nested node-local paths and project nested capability schemas through one
shared bounded JSON Schema path module. CLI and agent guidance distinguish
rooted bind endpoints from implied rootless map targets. Implementation:
[`nested local draft bindings`](historical/superpowers/plans/2026-07-22-nested-local-draft-bindings.md).
- Completed: `wf draft set-input` and `wf draft set-output` now accept - Completed: `wf draft set-input` and `wf draft set-output` now accept
`--merge`, preserving existing bindings when agents split map edits across `--merge`, preserving existing bindings when agents split map edits across
multiple revisions. multiple revisions.
@@ -555,7 +555,7 @@
- Closes only: nested bind rejection, capability-add nested projection skip, and stale CLI/agent guidance. - Closes only: nested bind rejection, capability-add nested projection skip, and stale CLI/agent guidance.
- Leaves open: structured-input assembly, literals, fan-out maps, nested workflow-output source projection, metadata/update-step, and TypeScript parity. - Leaves open: structured-input assembly, literals, fan-out maps, nested workflow-output source projection, metadata/update-step, and TypeScript parity.
- [ ] **Step 1: Update CLI and agent-facing examples** - [x] **Step 1: Update CLI and agent-facing examples**
Replace “bare field” wording with “rootless node-local path” in all three live user-facing documents. Include both forms together: Replace “bare field” wording with “rootless node-local path” in all three live user-facing documents. Include both forms together:
@@ -569,7 +569,7 @@
Explain that `wf draft bind` names both rooted endpoints, while `set-input` and capability-add already imply the local side and therefore accept rootless targets. Do not claim support for literals, fan-out, or atomic object assembly. Explain that `wf draft bind` names both rooted endpoints, while `set-input` and capability-add already imply the local side and therefore accept rootless targets. Do not claim support for literals, fan-out, or atomic object assembly.
- [ ] **Step 2: Update issue and roadmap state precisely** - [x] **Step 2: Update issue and roadmap state precisely**
In `ISSUES.md`, check only these three entries: In `ISSUES.md`, check only these three entries:
@@ -585,7 +585,7 @@
historical/superpowers/plans/2026-07-22-nested-local-draft-bindings.md historical/superpowers/plans/2026-07-22-nested-local-draft-bindings.md
``` ```
- [ ] **Step 3: Run the complete focused verification matrix** - [x] **Step 3: Run the complete focused verification matrix**
Run: Run:
@@ -599,7 +599,7 @@
Expected: all focused tests and static checks pass. The pytest count may increase as tests are added; report exact totals rather than copying a planned count. Expected: all focused tests and static checks pass. The pytest count may increase as tests are added; report exact totals rather than copying a planned count.
- [ ] **Step 4: Run an independent two-axis review** - [x] **Step 4: Run an independent two-axis review**
Use the `requesting-code-review` skill against the implementation start commit. Review: Use the `requesting-code-review` skill against the implementation start commit. Review:
@@ -608,7 +608,7 @@
Fix Critical and Important findings, rerun affected focused tests, and record any intentional Minor deferrals in the final report. Fix Critical and Important findings, rerun affected focused tests, and record any intentional Minor deferrals in the final report.
- [ ] **Step 5: Archive the completed plan and commit documentation** - [x] **Step 5: Archive the completed plan and commit documentation**
After all checks pass: After all checks pass:
+12 -4
View File
@@ -328,9 +328,11 @@ wf draft compile concat_ws
``` ```
`set-input` maps graph source paths to node-local input fields: `set-input` maps graph source paths to node-local input fields:
`input.text=text` means `input.text -> local.text`. Targets are bare `input.title=report.title` means
node-local field names: write `--map input.text=text`, not `input.title -> local.report.title`. Targets are rootless node-local paths:
`--map input.text=local.text`. write `--map input.title=report.title`, not
`--map input.title=local.report.title`. Existing single-field targets such as
`input.text=text` remain valid.
`set-output` maps node-local output fields to workflow state paths: `set-output` maps node-local output fields to workflow state paths:
`text=state.text` means `local.text -> state.text`. `text=state.text` means `local.text -> state.text`.
@@ -360,9 +362,15 @@ use `input.*` or `state.*` to `local.*` for step inputs, and `local.*` to
wf draft bind concat_ws --revision 9 --step call --from local.value --to state.value wf draft bind concat_ws --revision 9 --step call --from local.value --to state.value
wf draft bind concat_ws --revision 9 --step call --from input.text --to local.text wf draft bind concat_ws --revision 9 --step call --from input.text --to local.text
wf draft bind concat_ws --revision 9 --step call --from local.result --to output.result wf draft bind concat_ws --revision 9 --step call --from local.result --to output.result
wf draft bind report_ws --revision 2 --step render --from input.title --to local.report.title
wf draft set-input report_ws --revision 3 --step render --map input.title=report.title
wf draft validate concat_ws wf draft validate concat_ws
``` ```
`bind` names both endpoints, so nested local paths use the explicit `local.`
root. `set-input` and `draft add capability --input` already imply the local
side and therefore use rootless targets such as `report.title`.
If the workflow schema field already exists, `bind` reuses it and only updates If the workflow schema field already exists, `bind` reuses it and only updates
the step binding. Use `set-input --merge` for pure input-map edits when no the step binding. Use `set-input --merge` for pure input-map edits when no
schema projection is needed. schema projection is needed.
@@ -383,7 +391,7 @@ wf draft set-workflow-output report_ws --revision 6 --map state.markdown=markdow
The command combines two common edits: The command combines two common edits:
- It copies the selected capability local field schema into the workflow input, - It copies the selected capability local path schema into the workflow input,
state, or output schema at the graph path. state, or output schema at the graph path.
- It merges the matching step input or output binding. - It merges the matching step input or output binding.
+9 -4
View File
@@ -42,7 +42,7 @@ wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-start <workspace_id> --revision <n> --step <step_id> wf draft set-start <workspace_id> --revision <n> --step <step_id>
wf draft set-contract <workspace_id> --revision <n> --state-schema-file state.schema.json --outcome ok --outcome error wf draft set-contract <workspace_id> --revision <n> --state-schema-file state.schema.json --outcome ok --outcome error
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome <outcome> --to <target> wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome <outcome> --to <target>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.title=report.title
wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
@@ -51,9 +51,9 @@ wf draft set-workflow-output <workspace_id> --revision <n> --merge --map state.o
wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=fail wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=fail
wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
wf draft compile <workspace_id> wf draft compile <workspace_id>
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.report.markdown --to state.report.markdown
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.title --to local.report.title
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.title=report.title --bind-output result=state.result
wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review \ wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review \
--request-schema-file request.schema.json --resume-schema-file resume.schema.json \ --request-schema-file request.schema.json --resume-schema-file resume.schema.json \
--outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
@@ -80,6 +80,11 @@ existing step when schema projection may be needed; it is safe if the schema
field already exists. Use `wf draft set-input --merge --map input.x=x` for a field already exists. Use `wf draft set-input --merge --map input.x=x` for a
pure input-map edit when the workflow schema is already declared. pure input-map edit when the workflow schema is already declared.
`wf draft bind` names both endpoints explicitly, so local paths keep the
`local.` root. `set-input` and `draft add capability --input` already imply the
local side, so their targets are rootless paths: write
`input.title=report.title`, not `input.title=local.report.title`.
`wf draft set-workflow-output` projects missing public output schema fields for `wf draft set-workflow-output` projects missing public output schema fields for
single-field `input.*` and `state.*` sources. Prefer it for final workflow single-field `input.*` and `state.*` sources. Prefer it for final workflow
outputs; use `wf draft bind --from local.x --to output.y` when the source is a outputs; use `wf draft bind --from local.x --to output.y` when the source is a
@@ -99,7 +99,7 @@ CLI equivalents:
```bash ```bash
wf draft set-name <workspace_id> --revision <n> --name <name> wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target> wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.title=report.title
wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
@@ -108,9 +108,9 @@ wf draft set-workflow-output <workspace_id> --revision <n> --merge --map state.o
wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=fail wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=fail
wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
wf draft compile <workspace_id> wf draft compile <workspace_id>
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.report.markdown --to state.report.markdown
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.title --to local.report.title
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.title=report.title --bind-output result=state.result
wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review --request-schema-file request.schema.json --resume-schema-file resume.schema.json --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review --request-schema-file request.schema.json --resume-schema-file resume.schema.json --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
``` ```
@@ -121,9 +121,10 @@ wf draft add when <workspace_id> --revision <n> --step decide --condition-file c
For single-field `input.*` and `state.*` sources, missing public output schema For single-field `input.*` and `state.*` sources, missing public output schema
fields are projected automatically from the source schema. fields are projected automatically from the source schema.
`set-input` direction: `input.text=text` means graph source `input.text` maps to `set-input` direction: `input.title=report.title` means graph source
node-local target `local.text`. Targets are bare node-local field names; never `input.title` maps to node-local target `local.report.title`. Targets are
prefix the target with `local.`. rootless node-local paths; never prefix the target with `local.`. Existing
single-field targets such as `input.text=text` remain valid.
`set-output` direction: `text=state.text` means node-local source `local.text` `set-output` direction: `text=state.text` means node-local source `local.text`
maps to graph target `state.text`. maps to graph target `state.text`.
@@ -133,10 +134,11 @@ the whole map for that step or output scope. Use repeated `--map` flags in one
command for a complete replacement. Use `--merge` only when adding/updating command for a complete replacement. Use `--merge` only when adding/updating
entries over multiple revisions. entries over multiple revisions.
`bind input.x -> local.x` is schema-aware and idempotent when `input.x` is `bind input.title -> local.report.title` is schema-aware and idempotent when
already declared. Use it for repair hints or schema projection. Use `input.title` is already declared. Bind names both rooted endpoints explicitly.
`set-input --merge --map input.x=x` when you only need to update a step input Use it for repair hints or schema projection. Use
map. `set-input --merge --map input.title=report.title` when you only need to update
a step input map; that command already implies the local side.
- `bind_draft` - `bind_draft`
@@ -152,9 +154,9 @@ map.
workflow boundary. workflow boundary.
```bash ```bash
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.report.markdown --to state.report.markdown
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.title --to local.report.title
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to output.<field> wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.report.markdown --to output.report.markdown
wf draft validate <workspace_id> wf draft validate <workspace_id>
``` ```
@@ -172,9 +174,9 @@ wf draft validate <workspace_id>
unknown route entries and add one route for each missing declared outcome. It unknown route entries and add one route for each missing declared outcome. It
still requires explicit choices; if you do not still requires explicit choices; if you do not
know a map, inspect the capability or run validation rather than guessing. know a map, inspect the capability or run validation rather than guessing.
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings Explicit `--input input.title=report.title` and
project the corresponding workflow input/state schema fields from the `--input state.title=report.title` mappings project the corresponding
capability input schema. workflow input/state schema paths from the nested capability input schema.
```bash ```bash
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
+4
View File
@@ -604,6 +604,10 @@ class WorkflowDraftAuthoringApi:
continue continue
if source_root not in {"input", "state"}: if source_root not in {"input", "state"}:
continue continue
if not local_parts:
# `.` binds the whole graph value to the whole node input. It has
# no capability-property path from which to project one field.
continue
schema_key = "input_schema" if source_root == "input" else "state_schema" schema_key = "input_schema" if source_root == "input" else "state_schema"
target_schema = ( target_schema = (
projected_input_schema projected_input_schema
+33
View File
@@ -1461,6 +1461,39 @@ async def test_add_step_from_capability_projects_nested_local_input(
assert validated["status"] == "valid", validated["diagnostics"] assert validated["status"] == "valid", validated["diagnostics"]
@pytest.mark.asyncio
async def test_add_step_from_capability_preserves_whole_payload_input(
tmp_path: Path,
) -> None:
api, service, authoring = _draft_api(
FileWorkflowArtifactStore(tmp_path / "whole_payload_capability_input"),
register_echo=True,
)
service.register_specs("demo.personal", _nested_report)
draft = _nested_report_draft()
draft["steps"] = {}
draft["routes"] = {}
await api.create_draft_workspace(workspace_id="whole_payload", draft=draft)
result = await authoring.add_step_from_capability(
workspace_id="whole_payload",
revision=1,
step_id="render",
capability_name="demo.personal.nested_report",
routes={"ok": "__end__"},
input_map={"input.payload": "."},
bind_outputs={},
)
workspace = await api.get_draft_workspace(
workspace_id="whole_payload", include_draft=True
)
assert result["revision"] == 2
assert workspace["draft"]["steps"]["render"]["input"] == [
{"target": ".", "path": "input.payload"}
]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_step_from_capability_rejects_existing_step_id( async def test_add_step_from_capability_rejects_existing_step_id(
tmp_path: Path, tmp_path: Path,