docs: archive completed superpowers plans
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
# wf_api Slice 4B: Draft Service Extraction 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:** Move draft validation, draft workspace editing, and minimal draft bootstrapping out of `WorkflowSurfaceHandlers` into a protocol-neutral `wf_api.drafts.WorkflowDraftApi`.
|
||||
|
||||
**Architecture:** `WorkflowDraftApi` depends on `WorkflowOperationContext`, not `WfMcpService`. `WorkflowSurfaceHandlers` keeps the public method names and delegates the draft subset to `WorkflowDraftApi`. This is the first real method-body extraction, so the slice intentionally excludes methods that save artifacts, record events, or call `inspect_capability`.
|
||||
|
||||
**Tech Stack:** Python 3.14+, `wf_api.operation_context`, `wf_artifacts` draft helpers, `wf_core` path/binding models, pytest, ruff, basedpyright.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### Move In This Slice
|
||||
|
||||
Move these methods from `WorkflowSurfaceHandlers` to `wf_api.drafts.WorkflowDraftApi`:
|
||||
|
||||
```text
|
||||
validate_draft
|
||||
compile_draft
|
||||
patch_draft
|
||||
list_draft_workspaces
|
||||
create_draft_workspace
|
||||
get_draft_workspace
|
||||
delete_draft_workspace
|
||||
validate_draft_workspace
|
||||
patch_draft_workspace
|
||||
set_draft_name
|
||||
set_draft_route
|
||||
set_step_input_map
|
||||
set_step_output_map
|
||||
create_minimal_draft_workspace
|
||||
```
|
||||
|
||||
Move these draft-only helper functions to `wf_api.drafts`:
|
||||
|
||||
```text
|
||||
_required_capabilities_for_plan
|
||||
_required_capability_payloads
|
||||
_observed_node_specs
|
||||
_draft_input_maps
|
||||
_draft_output_map
|
||||
_draft_input_bindings_payload
|
||||
_draft_output_bindings_payload
|
||||
_graph_path_payload
|
||||
_local_path_payload
|
||||
_state_path_payload
|
||||
_escape_json_pointer
|
||||
```
|
||||
|
||||
### Do Not Move In This Slice
|
||||
|
||||
Do not move:
|
||||
|
||||
```text
|
||||
create_draft_workspace_from_capability
|
||||
create_artifact_from_draft
|
||||
create_artifact_from_workspace
|
||||
create_wrapper_from_workspace
|
||||
```
|
||||
|
||||
Reasons:
|
||||
|
||||
- `create_draft_workspace_from_capability` depends on `inspect_capability`, which belongs to the capability domain. Move it later after the capability-inspection seam is explicit.
|
||||
- artifact-from-draft/workspace methods save artifacts and record events. Move them with artifacts/deployments in Slice 4C.
|
||||
|
||||
Temporary duplication is allowed for private helper functions used by both
|
||||
draft preview and artifact creation. If a helper still has live callers in
|
||||
`WorkflowSurfaceHandlers` after draft delegation, keep the old copy until Slice
|
||||
4C moves the artifact/deployment methods. Do not make `wf_api` import
|
||||
`wf_mcp` just to avoid duplication.
|
||||
|
||||
### Invariants
|
||||
|
||||
- No public payload changes.
|
||||
- No MCP tool schema changes.
|
||||
- `WorkflowSurfaceHandlers` still exposes the same methods.
|
||||
- `wf_api` imports no `wf_mcp`.
|
||||
- Draft methods delegate through `WorkflowDraftApi`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create `wf_api.drafts`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `src/wf_api/drafts.py`
|
||||
|
||||
- [ ] **Step 1: Create `WorkflowDraftApi` skeleton**
|
||||
|
||||
Create `src/wf_api/drafts.py` with imports and class skeleton:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from wf_artifacts import (
|
||||
DraftWorkspaceStore,
|
||||
RequiredCapability,
|
||||
build_workflow_artifact_from_plan,
|
||||
compile_workflow_draft,
|
||||
create_draft_workspace as create_draft_workspace_record,
|
||||
get_draft_workspace as get_draft_workspace_record,
|
||||
patch_draft_workspace as patch_draft_workspace_record,
|
||||
patch_workflow_draft,
|
||||
validate_workflow_draft,
|
||||
)
|
||||
from wf_core.models.steps import (
|
||||
InputBinding,
|
||||
InputPathBinding,
|
||||
InputValueBinding,
|
||||
OutputBinding,
|
||||
)
|
||||
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||
from wf_platform import CapabilityRef, NodeSpecInventory
|
||||
|
||||
from .constants import (
|
||||
DEFAULT_CALL_STEP_ID,
|
||||
DEFAULT_ERROR_OUTCOME,
|
||||
DEFAULT_ERROR_STEP_ID,
|
||||
DEFAULT_OK_OUTCOME,
|
||||
RUNTIME_ERROR_CAPABILITY,
|
||||
)
|
||||
from .operation_context import WorkflowOperationContext
|
||||
|
||||
|
||||
class WorkflowDraftApi:
|
||||
"""Draft validation and workspace editing operations.
|
||||
|
||||
This service deliberately excludes artifact persistence and capability
|
||||
inspection. Those domains still live in the MCP-backed handler until later
|
||||
extraction slices.
|
||||
"""
|
||||
|
||||
def __init__(self, context: WorkflowOperationContext) -> None:
|
||||
self.context = context
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Export the draft service**
|
||||
|
||||
Add `WorkflowDraftApi` to `src/wf_api/__init__.py` so future adapters can use
|
||||
the canonical import path:
|
||||
|
||||
```python
|
||||
from .drafts import WorkflowDraftApi
|
||||
```
|
||||
|
||||
Also add `"WorkflowDraftApi"` to `__all__`.
|
||||
|
||||
- [ ] **Step 3: Add store helper**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
def _draft_store(self) -> DraftWorkspaceStore:
|
||||
if self.context.draft_workspace_store is None:
|
||||
raise KeyError("draft workspace store is not configured")
|
||||
return self.context.draft_workspace_store
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add outcome lookup helper**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
def _outcomes_for_capability(self, qualified_name: str) -> tuple[str, ...] | None:
|
||||
try:
|
||||
spec = self.context.specs.get_qualified_spec(qualified_name)
|
||||
except KeyError:
|
||||
return None
|
||||
outcomes = getattr(spec, "outcomes", None)
|
||||
return tuple(outcomes) if outcomes is not None else None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Move Stateless Draft Methods
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_api/drafts.py`
|
||||
|
||||
- [ ] **Step 1: Add `validate_draft`**
|
||||
|
||||
```python
|
||||
async def validate_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
||||
return validate_workflow_draft(
|
||||
draft,
|
||||
outcome_lookup=self._outcomes_for_capability,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `compile_draft`**
|
||||
|
||||
```python
|
||||
async def compile_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
||||
plan = compile_workflow_draft(draft)
|
||||
return {
|
||||
"compiled_plan": plan,
|
||||
"required_capabilities": _required_capability_payloads(
|
||||
_required_capabilities_for_plan(
|
||||
plan,
|
||||
source_bindings=None,
|
||||
context=self.context,
|
||||
)
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `patch_draft`**
|
||||
|
||||
```python
|
||||
async def patch_draft(
|
||||
self,
|
||||
*,
|
||||
draft: dict[str, Any],
|
||||
patch: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
return patch_workflow_draft(draft, patch)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Move Draft Workspace Methods
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_api/drafts.py`
|
||||
|
||||
- [ ] **Step 1: Add workspace CRUD and validation methods**
|
||||
|
||||
Move these method bodies exactly from `WorkflowSurfaceHandlers`, replacing
|
||||
`self._draft_store()` with the new `WorkflowDraftApi._draft_store()`:
|
||||
|
||||
```text
|
||||
list_draft_workspaces
|
||||
create_draft_workspace
|
||||
get_draft_workspace
|
||||
delete_draft_workspace
|
||||
validate_draft_workspace
|
||||
patch_draft_workspace
|
||||
```
|
||||
|
||||
Keep behavior and return payloads identical.
|
||||
|
||||
- [ ] **Step 2: Add patch convenience methods**
|
||||
|
||||
Move these method bodies exactly:
|
||||
|
||||
```text
|
||||
set_draft_name
|
||||
set_draft_route
|
||||
set_step_input_map
|
||||
set_step_output_map
|
||||
```
|
||||
|
||||
They should call `self.patch_draft_workspace(...)` inside `WorkflowDraftApi`.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Move Minimal Draft Bootstrap
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_api/drafts.py`
|
||||
|
||||
- [ ] **Step 1: Add `create_minimal_draft_workspace`**
|
||||
|
||||
Move `WorkflowSurfaceHandlers.create_minimal_draft_workspace` into
|
||||
`WorkflowDraftApi` unchanged except:
|
||||
|
||||
- use `self._outcomes_for_capability(...)`
|
||||
- use `self.create_draft_workspace(...)`
|
||||
- keep the existing comments about provider-specific error envelopes
|
||||
|
||||
- [ ] **Step 2: Add helper functions**
|
||||
|
||||
Move these helper functions from `handlers.py` to the bottom of `wf_api.drafts`:
|
||||
|
||||
```text
|
||||
_draft_input_maps
|
||||
_draft_output_map
|
||||
_draft_input_bindings_payload
|
||||
_draft_output_bindings_payload
|
||||
_graph_path_payload
|
||||
_local_path_payload
|
||||
_state_path_payload
|
||||
_escape_json_pointer
|
||||
```
|
||||
|
||||
Do not change their behavior.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Move Required-Capability Draft Helpers
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_api/drafts.py`
|
||||
|
||||
- [ ] **Step 1: Move `_required_capabilities_for_plan`**
|
||||
|
||||
Move the helper from `handlers.py` and change its signature from:
|
||||
|
||||
```python
|
||||
def _required_capabilities_for_plan(
|
||||
plan: dict[str, Any],
|
||||
*,
|
||||
source_bindings: dict[str, str] | None,
|
||||
service: WfMcpService,
|
||||
) -> dict[str, RequiredCapability]:
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
def _required_capabilities_for_plan(
|
||||
plan: dict[str, Any],
|
||||
*,
|
||||
source_bindings: dict[str, str] | None,
|
||||
context: WorkflowOperationContext,
|
||||
) -> dict[str, RequiredCapability]:
|
||||
```
|
||||
|
||||
Inside it, call `_observed_node_specs(context)` instead of
|
||||
`_observed_node_specs(service)`.
|
||||
|
||||
- [ ] **Step 2: Move `_observed_node_specs`**
|
||||
|
||||
Change its signature from:
|
||||
|
||||
```python
|
||||
def _observed_node_specs(service: WfMcpService) -> dict[str, NodeSpecInventory]:
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
def _observed_node_specs(
|
||||
context: WorkflowOperationContext,
|
||||
) -> dict[str, NodeSpecInventory]:
|
||||
```
|
||||
|
||||
Loop over `context.capability_sources.values()`.
|
||||
|
||||
- [ ] **Step 3: Move `_required_capability_payloads`**
|
||||
|
||||
Move it unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Wire `WorkflowSurfaceHandlers` To Delegate Draft Methods
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
||||
|
||||
- [ ] **Step 1: Add imports**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
from wf_api.drafts import WorkflowDraftApi
|
||||
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Instantiate draft service**
|
||||
|
||||
In `WorkflowSurfaceHandlers.__init__`, add:
|
||||
|
||||
```python
|
||||
self._drafts = WorkflowDraftApi(context_from_service(service))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace moved method bodies with delegates**
|
||||
|
||||
For each moved method, keep the same signature and replace the body with a call
|
||||
to `self._drafts`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
async def validate_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
||||
return await self._drafts.validate_draft(draft=draft)
|
||||
```
|
||||
|
||||
Apply this pattern to every method in the Slice 4B move list.
|
||||
|
||||
- [ ] **Step 4: Remove moved helper functions from `handlers.py`**
|
||||
|
||||
Delete only helper functions that are no longer used in `handlers.py`:
|
||||
|
||||
```text
|
||||
_draft_input_maps
|
||||
_draft_output_map
|
||||
_draft_input_bindings_payload
|
||||
_draft_output_bindings_payload
|
||||
_graph_path_payload
|
||||
_local_path_payload
|
||||
_state_path_payload
|
||||
```
|
||||
|
||||
Remove `_escape_json_pointer` only if no remaining references exist in `handlers.py`.
|
||||
|
||||
Do not remove from `handlers.py` yet unless `rg` proves there are no remaining
|
||||
callers:
|
||||
|
||||
```text
|
||||
_required_capabilities_for_plan
|
||||
_required_capability_payloads
|
||||
_observed_node_specs
|
||||
```
|
||||
|
||||
Artifact creation currently still uses these helpers. It is acceptable for
|
||||
`wf_api.drafts` and `handlers.py` to each have a copy until Slice 4C moves the
|
||||
artifact/deployment methods. If the implementor can safely share them from
|
||||
`wf_api` without creating an MCP import or changing behavior, that is allowed,
|
||||
but not required for this slice.
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Add Focused Tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tests/wf_api/test_drafts_service.py`
|
||||
|
||||
- [ ] **Step 1: Write direct service tests**
|
||||
|
||||
Create tests that build a `WfMcpService` through existing test helpers or
|
||||
`load_cli_context`, adapt it with `context_from_service`, then instantiate
|
||||
`WorkflowDraftApi`.
|
||||
|
||||
Cover:
|
||||
|
||||
- `patch_draft` applies a JSON patch.
|
||||
- `create_draft_workspace` creates a workspace.
|
||||
- `patch_draft_workspace` updates revision.
|
||||
- `validate_draft_workspace` refreshes status.
|
||||
- `create_minimal_draft_workspace` returns the same shape as before for a simple `wf.std` capability or a registered test spec.
|
||||
|
||||
- [ ] **Step 2: Add delegation smoke test**
|
||||
|
||||
In an existing workflow-surface draft test file or a new focused test, assert
|
||||
that `WorkflowSurfaceHandlers.validate_draft(...)` and
|
||||
`WorkflowDraftApi.validate_draft(...)` return equivalent status/diagnostics for
|
||||
the same draft.
|
||||
|
||||
Do not assert entire dict equality; compare stable fields individually.
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Verification
|
||||
|
||||
- [ ] **Step 1: Run draft tests**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_mcp/workflow_surface/test_drafts.py -q
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 2: Run MCP schema/config tests**
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/server/test_config.py tests/wf_mcp/workflow_surface -q
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 3: Run ruff on touched files**
|
||||
|
||||
```powershell
|
||||
uv run ruff check src/wf_api/drafts.py src/wf_mcp/workflow_surface/handlers.py tests/wf_api/test_drafts_service.py
|
||||
```
|
||||
|
||||
Expected: all checks pass.
|
||||
|
||||
- [ ] **Step 4: Run basedpyright on touched files**
|
||||
|
||||
```powershell
|
||||
uv run basedpyright --level error src/wf_api/drafts.py src/wf_mcp/workflow_surface/handlers.py tests/wf_api/test_drafts_service.py
|
||||
```
|
||||
|
||||
Expected: `0 errors`.
|
||||
|
||||
- [ ] **Step 5: Optional full suite**
|
||||
|
||||
```powershell
|
||||
uv run pytest -q
|
||||
```
|
||||
|
||||
Expected: full suite passes with the project’s existing skipped/xfailed counts.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- `wf_api.drafts` imports no `wf_mcp`.
|
||||
- `WorkflowSurfaceHandlers` public draft method signatures are unchanged.
|
||||
- Moved methods delegate through `WorkflowDraftApi`.
|
||||
- `create_draft_workspace_from_capability` remains in handlers.
|
||||
- artifact-saving methods remain in handlers.
|
||||
- No public payload shape changed.
|
||||
- No MCP schema changed.
|
||||
- No new dependency on the whole `WfMcpService` inside `wf_api`.
|
||||
Reference in New Issue
Block a user