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
- [ ] `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
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
segment.
- [ ] 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
`state.report.title`, leaving callers to patch the output schema manually even
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
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
guard. Implementation:
[`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
`--merge`, preserving existing bindings when agents split map edits across
multiple revisions.
@@ -555,7 +555,7 @@
- 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.
- [ ] **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:
@@ -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.
- [ ] **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:
@@ -585,7 +585,7 @@
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:
@@ -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.
- [ ] **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:
@@ -608,7 +608,7 @@
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:
+12 -4
View File
@@ -328,9 +328,11 @@ wf draft compile concat_ws
```
`set-input` maps graph source paths to node-local input fields:
`input.text=text` means `input.text -> local.text`. Targets are bare
node-local field names: write `--map input.text=text`, not
`--map input.text=local.text`.
`input.title=report.title` means
`input.title -> local.report.title`. Targets are rootless node-local paths:
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:
`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 input.text --to local.text
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
```
`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
the step binding. Use `set-input --merge` for pure input-map edits when no
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:
- 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.
- 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-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-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-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
@@ -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 handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
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 input.<field> --to local.<field>
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 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.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.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
@@ -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
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
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
@@ -99,7 +99,7 @@ CLI equivalents:
```bash
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-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-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
@@ -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 handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
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 input.<field> --to local.<field>
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 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.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.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 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
fields are projected automatically from the source schema.
`set-input` direction: `input.text=text` means graph source `input.text` maps to
node-local target `local.text`. Targets are bare node-local field names; never
prefix the target with `local.`.
`set-input` direction: `input.title=report.title` means graph source
`input.title` maps to node-local target `local.report.title`. Targets are
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`
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
entries over multiple revisions.
`bind input.x -> local.x` is schema-aware and idempotent when `input.x` is
already declared. Use it for repair hints or schema projection. Use
`set-input --merge --map input.x=x` when you only need to update a step input
map.
`bind input.title -> local.report.title` is schema-aware and idempotent when
`input.title` is already declared. Bind names both rooted endpoints explicitly.
Use it for repair hints or schema projection. Use
`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`
@@ -152,9 +154,9 @@ map.
workflow boundary.
```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 input.<field> --to local.<field>
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 state.report.markdown
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.report.markdown --to output.report.markdown
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
still requires explicit choices; if you do not
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
project the corresponding workflow input/state schema fields from the
capability input schema.
Explicit `--input input.title=report.title` and
`--input state.title=report.title` mappings project the corresponding
workflow input/state schema paths from the nested capability input schema.
```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
+4
View File
@@ -604,6 +604,10 @@ class WorkflowDraftAuthoringApi:
continue
if source_root not in {"input", "state"}:
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"
target_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"]
@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
async def test_add_step_from_capability_rejects_existing_step_id(
tmp_path: Path,