docs: archive idempotent draft bind plan
This commit is contained in:
@@ -0,0 +1,425 @@
|
|||||||
|
# Idempotent Draft Bind Inputs Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make `wf draft bind --from input.x --to local.x` and `wf draft bind --from state.x --to local.x` safe when the workflow schema path is already declared, while preserving real schema-conflict failures.
|
||||||
|
|
||||||
|
**Architecture:** Extend the existing semantic authoring service instead of adding another command. `bind_draft` already owns capability-aware schema projection and step input/output map mutation; this slice changes only the input-side projection policy to skip schema projection when the graph source path already exists, then updates CLI/help/skills so agents understand `bind` versus `set-input`.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, pytest, Typer CLI, existing `wf_api.draft_authoring.WorkflowDraftAuthoringApi`, existing TOML path strings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Recent debug challenge runs succeeded but repeatedly reported this UX issue:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf draft bind <workspace> --revision 1 --step call --from input.text --to local.text
|
||||||
|
ValueError: schema path 'text' already exists
|
||||||
|
```
|
||||||
|
|
||||||
|
The command is semantically valid as a repair/idempotent operation: the draft already has `input_schema.properties.text`, and the caller still wants the step input map `input.text -> local.text`.
|
||||||
|
|
||||||
|
This is different from conflicting schema projection. If the workflow schema already has `text` with incompatible shape, this slice deliberately does **not** attempt semantic compatibility. The behavior should be:
|
||||||
|
|
||||||
|
- If `input.text` / `state.text` is missing, project it from the capability input field schema and set the step input map.
|
||||||
|
- If `input.text` / `state.text` already exists, skip projection and set the step input map.
|
||||||
|
- If an ancestor path is invalid, such as `state.session` being a string while binding `state.session.text`, keep failing.
|
||||||
|
- If the local input field does not exist on the capability, keep failing.
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
- Modify `src/wf_api/draft_authoring.py`
|
||||||
|
- Reuse or add a tiny `_schema_path_exists(...)` helper near the existing path helpers.
|
||||||
|
- In `WorkflowDraftAuthoringApi.bind_draft`, for `target_root == "local"` and `source_root in {"input", "state"}`, skip `project_property_to_schema_path(...)` when the graph source schema path already exists.
|
||||||
|
- Modify `src/wf_cli/commands/drafts.py`
|
||||||
|
- Clarify `bind` help: use it when schema projection may be needed; for pure map replacement/merge, use `set-input`.
|
||||||
|
- Modify `tests/wf_api/test_drafts_service.py`
|
||||||
|
- Add regression tests for idempotent input and state binds.
|
||||||
|
- Modify `tests/wf_cli/test_app.py`
|
||||||
|
- Add/extend bind help assertion.
|
||||||
|
- Modify `docs/wf_cli.md`
|
||||||
|
- Document idempotent bind behavior and `set-input` guidance.
|
||||||
|
- Modify `skills/wf-cli/SKILL.md`
|
||||||
|
- Teach agents: use `set-input --merge` for pure map edits; use `bind` when the schema may need projection or when following a repair hint.
|
||||||
|
- Modify `skills/wf-workflow/references/draft-workspaces.md`
|
||||||
|
- Mirror the same authoring guidance.
|
||||||
|
- Modify `docs/current_roadmap.md`
|
||||||
|
- Add a short completed bullet once implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add API Regression Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/wf_api/test_drafts_service.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add an input-schema idempotency regression test**
|
||||||
|
|
||||||
|
Add this test near `test_bind_draft_workflow_input_to_step_input_projects_input_schema`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bind_draft_workflow_input_to_step_input_reuses_existing_schema(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
tmp_path / "drafts_bind_existing_input_schema"
|
||||||
|
)
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="bind_ws",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await authoring.bind_draft(
|
||||||
|
workspace_id="bind_ws",
|
||||||
|
revision=1,
|
||||||
|
step_id="echo",
|
||||||
|
source_path="input.text",
|
||||||
|
target_path="local.text",
|
||||||
|
)
|
||||||
|
workspace = await api.get_draft_workspace(
|
||||||
|
workspace_id="bind_ws", include_draft=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "valid", result["diagnostics"]
|
||||||
|
assert result["revision"] == 2
|
||||||
|
assert workspace["draft"]["steps"]["echo"]["input"] == [
|
||||||
|
{"target": "text", "path": "input.text"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a state-schema idempotency regression test**
|
||||||
|
|
||||||
|
Add this test near the input-schema test:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bind_draft_workflow_state_to_step_input_reuses_existing_schema(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
tmp_path / "drafts_bind_existing_state_schema"
|
||||||
|
)
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
draft = {
|
||||||
|
**_echo_draft(),
|
||||||
|
"state_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"text": {"type": "string"}},
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"echo": {
|
||||||
|
"use": "demo.personal.echo_tool",
|
||||||
|
"input": [],
|
||||||
|
"output": [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await api.create_draft_workspace(workspace_id="bind_ws", draft=draft)
|
||||||
|
|
||||||
|
result = await authoring.bind_draft(
|
||||||
|
workspace_id="bind_ws",
|
||||||
|
revision=1,
|
||||||
|
step_id="echo",
|
||||||
|
source_path="state.text",
|
||||||
|
target_path="local.text",
|
||||||
|
)
|
||||||
|
workspace = await api.get_draft_workspace(
|
||||||
|
workspace_id="bind_ws", include_draft=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "valid", result["diagnostics"]
|
||||||
|
assert result["revision"] == 2
|
||||||
|
assert workspace["draft"]["steps"]["echo"]["input"] == [
|
||||||
|
{"target": "text", "path": "state.text"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py -q -k "bind_draft_workflow_input_to_step_input_reuses_existing_schema or bind_draft_workflow_state_to_step_input_reuses_existing_schema"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected before implementation: both tests fail with `ValueError: schema path 'text' already exists`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit failing tests**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add tests/wf_api/test_drafts_service.py
|
||||||
|
git commit -m "test: cover idempotent draft input bind"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Make Input-Side `bind_draft` Idempotent
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/draft_authoring.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add or reuse a schema-path existence helper**
|
||||||
|
|
||||||
|
If `_schema_path_exists` already exists in `src/wf_api/draft_authoring.py`, reuse it. If it does not, add this near `_local_parts`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _schema_path_exists(schema: Mapping[str, Any], parts: Sequence[str]) -> bool:
|
||||||
|
current: Any = schema
|
||||||
|
for part in parts:
|
||||||
|
if not isinstance(current, Mapping):
|
||||||
|
return False
|
||||||
|
properties = current.get("properties")
|
||||||
|
if not isinstance(properties, Mapping) or part not in properties:
|
||||||
|
return False
|
||||||
|
current = properties[part]
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure imports include `Mapping`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Skip projection for already-declared graph source paths**
|
||||||
|
|
||||||
|
In `WorkflowDraftAuthoringApi.bind_draft`, inside:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if target_root == "local" and source_root in {"input", "state"}:
|
||||||
|
```
|
||||||
|
|
||||||
|
replace the unconditional projection:
|
||||||
|
|
||||||
|
```python
|
||||||
|
projected = project_property_to_schema_path(
|
||||||
|
target_schema=target_schema,
|
||||||
|
source_schema=input_schema,
|
||||||
|
source_field=local_field,
|
||||||
|
target_parts=source_parts,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if _schema_path_exists(target_schema, source_parts):
|
||||||
|
projected = target_schema
|
||||||
|
else:
|
||||||
|
projected = project_property_to_schema_path(
|
||||||
|
target_schema=target_schema,
|
||||||
|
source_schema=input_schema,
|
||||||
|
source_field=local_field,
|
||||||
|
target_parts=source_parts,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the patch shape unchanged:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{"op": "replace", "path": f"/{schema_key}", "value": projected}
|
||||||
|
```
|
||||||
|
|
||||||
|
This may write the same schema back. That is acceptable for this slice because the operation still intentionally mutates the step input map and bumps the workspace revision.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run API tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py -q -k "bind_draft_workflow_input_to_step_input"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: projection and idempotency tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run broader draft focused tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_api/test_schema_projection.py -q -k "bind_draft or project_output_property or set_workflow_output_map or add_step_projects_explicit_optional_workflow_inputs"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all selected tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit implementation**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_api/draft_authoring.py tests/wf_api/test_drafts_service.py
|
||||||
|
git commit -m "fix: make draft input bind idempotent"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Clarify CLI Help And Agent-Facing Docs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_cli/commands/drafts.py`
|
||||||
|
- Modify: `tests/wf_cli/test_app.py`
|
||||||
|
- Modify: `docs/wf_cli.md`
|
||||||
|
- Modify: `skills/wf-cli/SKILL.md`
|
||||||
|
- Modify: `skills/wf-workflow/references/draft-workspaces.md`
|
||||||
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update CLI bind help text**
|
||||||
|
|
||||||
|
In `src/wf_cli/commands/drafts.py`, update the `bind_draft` docstring from:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Bind a capability step path and project the matching schema.
|
||||||
|
|
||||||
|
Direction matters. Use input/state -> local for step inputs and local ->
|
||||||
|
state/output for step outputs. Run `wf draft validate <workspace_id>` after
|
||||||
|
this command.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Bind a capability step path and project missing schema when needed.
|
||||||
|
|
||||||
|
Direction matters. Use input/state -> local for step inputs and local ->
|
||||||
|
state/output for step outputs. If the workflow schema field already exists,
|
||||||
|
the command reuses it and updates the step binding. For pure input-map edits
|
||||||
|
where schema is already known, `wf draft set-input --merge` is also valid.
|
||||||
|
Run `wf draft validate <workspace_id>` after this command.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update CLI help test**
|
||||||
|
|
||||||
|
In `tests/wf_cli/test_app.py`, find `test_wf_draft_bind_help_explains_direction` and add assertions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
help_text = " ".join(result.output.split())
|
||||||
|
assert "project missing schema" in help_text
|
||||||
|
assert "set-input --merge" in help_text
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep existing assertions.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update `docs/wf_cli.md`**
|
||||||
|
|
||||||
|
In the “Bind A Step Path” section, add this paragraph after the direction explanation:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update `skills/wf-cli/SKILL.md`**
|
||||||
|
|
||||||
|
Replace the current optional-input guidance with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Draft creation auto-binds required capability inputs only. Optional inputs are
|
||||||
|
reported in wrapper-hint notes; bind them explicitly only when the workflow
|
||||||
|
should expose them. Use `wf draft bind --from input.x --to local.x` for an
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update `skills/wf-workflow/references/draft-workspaces.md`**
|
||||||
|
|
||||||
|
Add this after the `set-input` direction paragraph:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
`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.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update roadmap**
|
||||||
|
|
||||||
|
Add a short completed bullet under the draft/CLI section of `docs/current_roadmap.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- Completed: `wf draft bind` now reuses existing workflow input/state schema
|
||||||
|
fields when binding to step-local inputs, avoiding redundant-schema failures
|
||||||
|
found by debug challenge runs.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run docs/help tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_cli/test_app.py::test_wf_draft_bind_help_explains_direction -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit docs/help**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_cli/commands/drafts.py tests/wf_cli/test_app.py docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md
|
||||||
|
git commit -m "docs: clarify draft bind input behavior"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Verification And Live Smoke
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- No source edits expected.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run focused verification**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py::test_wf_draft_bind_help_explains_direction -q
|
||||||
|
uv run ruff check src/wf_api/draft_authoring.py src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py
|
||||||
|
uv run ruff format --check src/wf_api/draft_authoring.py src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py
|
||||||
|
uv run basedpyright --level error src/wf_api/draft_authoring.py src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- pytest selected tests pass
|
||||||
|
- ruff check has no errors
|
||||||
|
- ruff format reports files already formatted
|
||||||
|
- basedpyright reports 0 errors
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run live RPC smoke if server is running**
|
||||||
|
|
||||||
|
If the user has `wf-rpc-server --config wf.config.json --host 127.0.0.1 --port 8765` running, use this smoke:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$id = 'smoke_bind_idem_' + (Get-Date -Format 'HHmmss')
|
||||||
|
uv run wf draft create $id --capability everything.default.echo --name $id
|
||||||
|
uv run wf draft bind $id --revision 1 --step call --from input.message --to local.message
|
||||||
|
uv run wf draft validate $id
|
||||||
|
uv run wf draft inspect $id --include-draft
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- `draft bind` returns `status: valid`, not `ValueError: schema path 'message' already exists`.
|
||||||
|
- inspect shows step `call.input` contains `{"path": "input.message", "target": "message"}`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Archive this plan after implementation**
|
||||||
|
|
||||||
|
Move this plan to historical:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git mv docs/superpowers/plans/2026-06-29-idempotent-draft-bind-inputs.md docs/historical/superpowers/plans/2026-06-29-idempotent-draft-bind-inputs.md
|
||||||
|
git commit -m "docs: archive idempotent draft bind plan"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- Spec coverage: This plan covers the observed duplicate-schema UX issue for `input/state -> local` binds, help/docs guidance, and live smoke. It does not implement composite object binding such as `state.title=report.title`; that is a separate data-shaping feature.
|
||||||
|
- Placeholder scan: No `TBD`, `TODO`, or vague “add tests” steps remain.
|
||||||
|
- Type consistency: The plan uses existing names: `WorkflowDraftAuthoringApi.bind_draft`, `_schema_path_exists`, `project_property_to_schema_path`, `wf draft bind`, and `wf draft set-input --merge`.
|
||||||
|
|
||||||
Reference in New Issue
Block a user