Files
lda-wf/docs/historical/superpowers/plans/2026-06-02-wf-api-store-ownership.md
T

512 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# wf_api Store Ownership 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 workflow artifact, draft workspace, and run store ownership explicit so `wf_api`, CLI, MCP, and future HTTP entrypoints can share stores without relying on `WfMcpService.__post_init__`.
**Architecture:** Add a protocol-neutral `WorkflowStores` bundle in `wf_api` that groups the three workflow stores. MCP config construction remains responsible for creating file-backed stores from `BrokerConfig.store_root`; `WfMcpService` receives stores but no longer manufactures them from its MCP `Store`. Existing process-local behavior stays intact through `build_service_from_config`.
**Tech Stack:** Python 3.14, dataclasses, `wf_api`, `wf_artifacts`, `wf_mcp`, pytest, ruff, basedpyright.
---
## Current Problem
`src/wf_mcp/broker/service/core.py` currently does this in `WfMcpService.__post_init__`:
```python
if self.artifact_store is None:
self.artifact_store = FileWorkflowArtifactStore(_store_root(self.store))
if self.draft_workspace_store is None:
self.draft_workspace_store = FileDraftWorkspaceStore(_store_root(self.store))
if self.run_store is None:
self.run_store = FileRunStore(_store_root(self.store))
```
That makes a protocol-specific service decide protocol-neutral workflow persistence. It also hides missing-store tests because `WfMcpService(store=FileStore(...))` silently creates workflow stores.
`src/wf_mcp/broker/config.py::build_service_from_config` already does the right thing by passing all three stores explicitly. This slice preserves that behavior and removes the fallback.
## Target Ownership Rule
- `wf_artifacts` owns store protocols and file store implementations.
- `wf_api` may group protocol-neutral workflow stores into a small DTO.
- `wf_mcp` owns MCP config loading and calls the DTO factory for file-backed process-local stores.
- `WfMcpService` owns broker state, connections, adapters, source catalogs, events, and execution wiring.
- `WfMcpService` does not create workflow artifact/draft/run stores by guessing from the MCP catalog/auth store.
## Files
- Create: `src/wf_api/stores.py`
- Modify: `src/wf_api/__init__.py`
- Modify: `src/wf_mcp/broker/config.py`
- Modify: `src/wf_mcp/broker/service/core.py`
- Modify: direct `WfMcpService(...)` tests only where they rely on implicit workflow stores
- Test: `tests/wf_api/test_stores.py`
- Test: `tests/wf_mcp/service/test_catalog.py`
- Test: `tests/wf_mcp/test_broker_server.py`
---
### Task 1: Add Protocol-Neutral Store Bundle
**Files:**
- Create: `src/wf_api/stores.py`
- Modify: `src/wf_api/__init__.py`
- Test: `tests/wf_api/test_stores.py`
- [ ] **Step 1: Write failing store bundle tests**
Create `tests/wf_api/test_stores.py`:
```python
from __future__ import annotations
from wf_api.stores import WorkflowStores, file_workflow_stores
from wf_artifacts import (
FileDraftWorkspaceStore,
FileRunStore,
FileWorkflowArtifactStore,
)
from tests.wf_mcp.test_support import local_temp_root
def test_file_workflow_stores_constructs_all_three_file_stores() -> None:
root = local_temp_root() / "wf_api_file_workflow_stores"
stores = file_workflow_stores(root)
assert isinstance(stores, WorkflowStores)
assert isinstance(stores.artifact_store, FileWorkflowArtifactStore)
assert isinstance(stores.draft_workspace_store, FileDraftWorkspaceStore)
assert isinstance(stores.run_store, FileRunStore)
assert stores.artifact_store.root == root
assert stores.draft_workspace_store.root == root
assert stores.run_store.root == root
def test_wf_api_exports_workflow_stores() -> None:
from wf_api import WorkflowStores as ExportedWorkflowStores
from wf_api import file_workflow_stores as exported_file_workflow_stores
assert ExportedWorkflowStores is WorkflowStores
assert exported_file_workflow_stores is file_workflow_stores
```
- [ ] **Step 2: Run failing tests**
Run:
```bash
uv run pytest tests\wf_api\test_stores.py -q
```
Expected: import failure for `wf_api.stores`.
- [ ] **Step 3: Implement `wf_api.stores`**
Create `src/wf_api/stores.py`:
```python
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from wf_artifacts import (
DraftWorkspaceStore,
FileDraftWorkspaceStore,
FileRunStore,
FileWorkflowArtifactStore,
RunStore,
WorkflowArtifactStore,
)
@dataclass(frozen=True, slots=True)
class WorkflowStores:
"""Protocol-neutral persistence dependencies for workflow APIs."""
artifact_store: WorkflowArtifactStore
draft_workspace_store: DraftWorkspaceStore
run_store: RunStore
def file_workflow_stores(root: str | Path) -> WorkflowStores:
"""Create process-local file-backed workflow stores under one root."""
store_root = Path(root)
return WorkflowStores(
artifact_store=FileWorkflowArtifactStore(store_root),
draft_workspace_store=FileDraftWorkspaceStore(store_root),
run_store=FileRunStore(store_root),
)
__all__ = ["WorkflowStores", "file_workflow_stores"]
```
- [ ] **Step 4: Export from `wf_api`**
Update `src/wf_api/__init__.py`:
```python
from .stores import WorkflowStores, file_workflow_stores
```
Add both names to `__all__`.
- [ ] **Step 5: Verify Task 1**
Run:
```bash
uv run pytest tests\wf_api\test_stores.py -q
uv run ruff check src\wf_api\stores.py tests\wf_api\test_stores.py
uv run ruff format --check src\wf_api\stores.py tests\wf_api\test_stores.py
```
Expected: tests pass, lint pass, format pass.
---
### Task 2: Move Config Store Construction Through the Bundle
**Files:**
- Modify: `src/wf_mcp/broker/config.py`
- Test: `tests/wf_mcp/test_broker_server.py`
- [ ] **Step 1: Strengthen config construction test**
Find `tests/wf_mcp/test_broker_server.py::test_build_service_from_config_uses_store_root_for_artifacts`.
Update it to assert all three stores use the configured root:
```python
def test_build_service_from_config_uses_store_root_for_workflow_stores() -> None:
store_root = local_temp_root() / "broker_config_workflow_stores"
config = BrokerConfig(store_root=store_root, connections=[])
service = build_service_from_config(config)
assert isinstance(service.artifact_store, FileWorkflowArtifactStore)
assert isinstance(service.draft_workspace_store, FileDraftWorkspaceStore)
assert isinstance(service.run_store, FileRunStore)
assert service.artifact_store.root == store_root
assert service.draft_workspace_store.root == store_root
assert service.run_store.root == store_root
```
Ensure the test imports:
```python
from wf_artifacts import FileDraftWorkspaceStore, FileRunStore, FileWorkflowArtifactStore
```
- [ ] **Step 2: Run the focused test**
Run:
```bash
uv run pytest tests\wf_mcp\test_broker_server.py::test_build_service_from_config_uses_store_root_for_workflow_stores -q
```
Expected: pass before the config refactor, proving current behavior is covered.
- [ ] **Step 3: Update `build_service_from_config` to use `file_workflow_stores`**
In `src/wf_mcp/broker/config.py`, replace direct file store imports:
```python
from wf_api import file_workflow_stores
```
Remove:
```python
from wf_artifacts import (
FileDraftWorkspaceStore,
FileRunStore,
FileWorkflowArtifactStore,
)
```
Then update `build_service_from_config`:
```python
def build_service_from_config(config: BrokerConfig) -> WfMcpService:
"""Create a broker service with SDK adapters for configured connections."""
runtime_factory = PersistentSessionFactory()
workflow_stores = file_workflow_stores(config.store_root)
service = WfMcpService(
store=FileStore(config.store_root),
artifact_store=workflow_stores.artifact_store,
draft_workspace_store=workflow_stores.draft_workspace_store,
run_store=workflow_stores.run_store,
# Discovery can use short-lived SDK sessions. Workflow execution needs
# a persistent runtime so stateful MCP servers keep session/page state
# across sequential workflow nodes.
tool_executor=McpRuntimePool(runtime_factory.create),
)
```
- [ ] **Step 4: Verify Task 2**
Run:
```bash
uv run pytest tests\wf_mcp\test_broker_server.py::test_build_service_from_config_uses_store_root_for_workflow_stores -q
uv run ruff check src\wf_mcp\broker\config.py tests\wf_mcp\test_broker_server.py
uv run ruff format --check src\wf_mcp\broker\config.py tests\wf_mcp\test_broker_server.py
```
Expected: tests pass, lint pass, format pass.
---
### Task 3: Remove Implicit Workflow Store Creation from WfMcpService
**Files:**
- Modify: `src/wf_mcp/broker/service/core.py`
- Modify: `tests/wf_mcp/service/test_catalog.py`
- [ ] **Step 1: Replace the old default-store test**
Find `tests/wf_mcp/service/test_catalog.py::test_service_installs_default_draft_workspace_store`.
Replace it with:
```python
def test_service_does_not_install_workflow_stores_implicitly() -> None:
root = local_temp_root() / "service_no_implicit_workflow_stores"
service = WfMcpService(store=FileStore(root))
assert service.artifact_store is None
assert service.draft_workspace_store is None
assert service.run_store is None
```
- [ ] **Step 2: Run failing test**
Run:
```bash
uv run pytest tests\wf_mcp\service\test_catalog.py::test_service_does_not_install_workflow_stores_implicitly -q
```
Expected: fail because `WfMcpService.__post_init__` still creates stores.
- [ ] **Step 3: Remove implicit creation from `WfMcpService.__post_init__`**
In `src/wf_mcp/broker/service/core.py`, remove the imports:
```python
FileDraftWorkspaceStore,
FileRunStore,
FileWorkflowArtifactStore,
```
Remove the `_store_root` helper entirely:
```python
def _store_root(store: Store) -> Path:
"""Return the file root for stores that expose one, else use local default."""
root = getattr(store, "root", None)
return root if isinstance(root, Path) else Path(".wf_mcp_store")
```
Update `WfMcpService.__post_init__` to:
```python
def __post_init__(self) -> None:
"""Install broker-local system specs when enabled.
Workflow stores are injected by entrypoint/config construction. This service
must not guess workflow persistence from the MCP catalog/auth store because
CLI, MCP, and future HTTP frontends may share or swap those stores.
"""
if self.include_builtin_specs:
for source in builtin_sources().values():
self.register_capability_source(source)
self.register_capability_source(admin_source())
```
If `Path` becomes unused in `core.py`, remove `from pathlib import Path`.
- [ ] **Step 4: Verify Task 3**
Run:
```bash
uv run pytest tests\wf_mcp\service\test_catalog.py::test_service_does_not_install_workflow_stores_implicitly -q
uv run ruff check src\wf_mcp\broker\service\core.py tests\wf_mcp\service\test_catalog.py
uv run ruff format --check src\wf_mcp\broker\service\core.py tests\wf_mcp\service\test_catalog.py
```
Expected: tests pass, lint pass, format pass.
---
### Task 4: Fix Direct Service Tests That Need Workflow Stores
**Files:**
- Modify only tests that fail after Task 3.
- [ ] **Step 1: Run targeted workflow API/service tests**
Run:
```bash
uv run pytest tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service -q
```
Expected: if failures appear, they should be tests that constructed `WfMcpService(store=...)` but then used workflow artifact/draft/run operations.
- [ ] **Step 2: Patch only failing tests by injecting stores explicitly**
For any failing direct `WfMcpService(...)` test that needs workflow stores, use this pattern:
```python
from wf_api import file_workflow_stores
root = local_temp_root() / "test_specific_name"
workflow_stores = file_workflow_stores(root)
service = WfMcpService(
store=FileStore(root / "mcp"),
artifact_store=workflow_stores.artifact_store,
draft_workspace_store=workflow_stores.draft_workspace_store,
run_store=workflow_stores.run_store,
)
```
Do not add stores to tests that only exercise broker catalog/admin/source behavior.
- [ ] **Step 3: Keep no-store behavior tests intact**
Tests like these should keep `artifact_store=None` through `WorkflowOperationContext` or direct service construction because they prove graceful no-store behavior:
```python
assert result["nodes"] == []
assert result["deployments"] == []
```
Do not “fix” these by injecting stores unless the test is explicitly about stored workflow data.
- [ ] **Step 4: Verify targeted tests**
Run:
```bash
uv run pytest tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service -q
uv run ruff check tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service
uv run ruff format --check tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service
```
Expected: targeted tests pass and no broad fixture churn.
---
### Task 5: Document the Store Ownership Rule
**Files:**
- Modify: `docs/superpowers/research/2026-06-01-wf-api-extraction-map.md`
- Modify: `docs/current_roadmap.md` if it already has a `wf_api` section
- [ ] **Step 1: Update the extraction map**
In `docs/superpowers/research/2026-06-01-wf-api-extraction-map.md`, under `### Store Ownership Ambiguity`, replace the section with:
```markdown
### Store Ownership
- `wf_artifacts` owns workflow store protocols and file-backed implementations.
- `wf_api.stores.WorkflowStores` groups the artifact, draft workspace, and run stores as protocol-neutral API dependencies.
- MCP config construction creates file-backed workflow stores from `BrokerConfig.store_root` and injects them into `WfMcpService`.
- `WfMcpService.__post_init__` no longer creates workflow stores from its MCP `Store`; direct service tests must inject stores when they exercise workflow persistence.
- Future HTTP/API entrypoints should construct or receive the same `WorkflowStores` bundle instead of importing `wf_mcp`.
```
- [ ] **Step 2: Update roadmap only if there is a matching section**
If `docs/current_roadmap.md` has a `wf_api` or API extraction section, add:
```markdown
- Workflow store ownership is explicit: entrypoints construct/inject `WorkflowStores`; `WfMcpService` no longer guesses stores from the MCP store root.
```
If there is no matching section, skip this file.
- [ ] **Step 3: Verify docs are not stale**
Run:
```bash
rg -n "_store_root\\(|creates default `FileWorkflowArtifactStore`|installs default.*store" src docs tests
```
Expected: no current docs/tests claim `WfMcpService` installs default workflow stores. Historical plans may still mention old implementation; leave historical plans alone unless they are current roadmap/research docs.
---
### Task 6: Final Verification
**Files:**
- All touched files.
- [ ] **Step 1: Run focused suite**
Run:
```bash
uv run pytest tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service -q
```
Expected: all selected tests pass.
- [ ] **Step 2: Run full suite**
Run:
```bash
uv run pytest -q
```
Expected: full suite passes with the repos known skip/xfail counts.
- [ ] **Step 3: Run lint and format checks**
Run:
```bash
uv run ruff check src\wf_api src\wf_mcp tests\wf_api tests\wf_mcp
uv run ruff format --check src\wf_api src\wf_mcp tests\wf_api tests\wf_mcp
```
Expected: all checks pass.
- [ ] **Step 4: Run typecheck**
Run:
```bash
uv run basedpyright --level error
```
Expected: `0 errors, 0 warnings, 0 notes`. If the command exits nonzero only because of the known workspace enumeration warning, report that exactly.
---
## Self-Review
- Spec coverage: This plan covers explicit store creation, `WfMcpService.__post_init__` cleanup, config behavior preservation, test migration, and docs.
- Placeholder scan: No `TODO`/`TBD` placeholders remain. Historical-plan references are explicitly scoped.
- Type consistency: `WorkflowStores` uses protocol types from `wf_artifacts`; `file_workflow_stores()` returns file-backed implementations; `WfMcpService` field types do not change.
- Scope check: This does not implement FastAPI, persisted-run conflict handling, or store locking. Those are separate slices.