more plan files
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
# MCP Frontend Structural Paths and Reducers 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:** Expose structural graph paths, canonical builder bindings, and structural reducer refs through the MCP/workflow frontend so LLM clients stop learning dotted-string separator conventions as canonical.
|
||||
|
||||
**Architecture:** Treat MCP as a frontend over the platform model, not the source of truth. The MCP tools should accept compatibility strings where existing users need them, but list/inspect/create responses should prefer canonical structs: `input` / `output` binding lists, path `{root, parts}` objects, and reducer refs with structural `ref` objects for configured reducers. Existing raw-plan escape hatches remain, but the recommended workflow-authoring path should produce canonical model-shaped JSON.
|
||||
|
||||
**Tech Stack:** Python 3.14, Pydantic v2, `wf_mcp.workflow_surface`, `wf_artifacts.drafts`, `wf_artifacts.factory`, `wf_core` models, pytest, basedpyright, ruff.
|
||||
|
||||
---
|
||||
|
||||
## Dependency
|
||||
|
||||
Run this plan **after**:
|
||||
|
||||
```text
|
||||
2026-05-21-reducer-ref-structural-capability.md
|
||||
```
|
||||
|
||||
because MCP should expose the final `ReducerRef` model shape, not invent a parallel frontend shape.
|
||||
|
||||
---
|
||||
|
||||
## Current Problems
|
||||
|
||||
MCP/workflow frontend still has several old shapes:
|
||||
|
||||
- workflow plans and draft APIs often show `in_map`, `input_values`, `out_map`
|
||||
- state schema examples still use legacy `fields`
|
||||
- reducer refs are often shown as dotted strings only
|
||||
- inspect/list tool outputs may not clearly distinguish canonical structs from display strings
|
||||
|
||||
The result: an LLM client can build runnable workflows, but it learns the wrong authoring shape and then has to guess separator semantics.
|
||||
|
||||
---
|
||||
|
||||
## Target Frontend Shape
|
||||
|
||||
Recommended node use shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "echo",
|
||||
"type": "node",
|
||||
"node": "demo.echo",
|
||||
"input": [
|
||||
{
|
||||
"target": {"root": "local", "parts": ["text"]},
|
||||
"path": {"root": "input", "parts": ["text"]}
|
||||
},
|
||||
{
|
||||
"target": {"root": "local", "parts": ["limit"]},
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"output": [
|
||||
{
|
||||
"source": {"root": "local", "parts": ["echoed"]},
|
||||
"target": {"root": "state", "parts": ["echoed"]}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Recommended configured reducer shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
|
||||
"config": {"modulus": 10}
|
||||
}
|
||||
```
|
||||
|
||||
Compact unconfigured reducer shorthand remains accepted:
|
||||
|
||||
```json
|
||||
"wf.std.add"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Inspect/modify: `src/wf_mcp/workflow_surface/models.py`
|
||||
- Request/response models for create/compile/validate/call workflow tools
|
||||
|
||||
- Inspect/modify: `src/wf_mcp/workflow_surface/handlers.py`
|
||||
- create draft/workflow helpers
|
||||
- source/capability inspection payloads
|
||||
|
||||
- Inspect/modify: `src/wf_mcp/workflow_surface/tools.py`
|
||||
- MCP tool schemas/descriptions
|
||||
|
||||
- Inspect/modify: `src/wf_artifacts/drafts/models.py`
|
||||
- draft step/input/output shape if drafts still generate raw maps
|
||||
|
||||
- Inspect/modify: `src/wf_artifacts/drafts/adapter.py`
|
||||
- draft-to-builder compile path; should use canonical `input` / `output`
|
||||
|
||||
- Inspect/modify docs:
|
||||
- `docs/wf_mcp_operator_manual.md`
|
||||
- `docs/workflow_drafts.md`
|
||||
- `docs/wf_mcp_end_to_end_runbook.md`
|
||||
- `docs/structural_refs.md`
|
||||
|
||||
- Tests:
|
||||
- `tests/wf_mcp/test_workflow_surface.py`
|
||||
- `tests/wf_mcp/test_workflow_wrapper_hints.py`
|
||||
- `tests/artifacts/test_draft_adapter.py`
|
||||
- `tests/artifacts/test_draft_models.py`
|
||||
- `tests/artifacts/test_draft_api.py`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Inventory MCP/Draft Surfaces That Emit Map Sugar
|
||||
|
||||
**Files:**
|
||||
- Read-only first:
|
||||
- `src/wf_mcp/workflow_surface/models.py`
|
||||
- `src/wf_mcp/workflow_surface/handlers.py`
|
||||
- `src/wf_artifacts/drafts/models.py`
|
||||
- `src/wf_artifacts/drafts/adapter.py`
|
||||
|
||||
- [ ] **Step 1: Search old map fields**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n '"in_map"|in_map|input_values|"out_map"|out_map|fields' src/wf_mcp src/wf_artifacts tests/wf_mcp tests/artifacts docs -g '*.py' -g '*.md'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Categorize each hit**
|
||||
|
||||
Use these categories:
|
||||
|
||||
- compatibility input still accepted
|
||||
- canonical output should be changed
|
||||
- test fixture using old shape intentionally
|
||||
- docs/example should migrate
|
||||
|
||||
- [ ] **Step 3: Write findings into this plan or a short docs note**
|
||||
|
||||
Add a small checklist under this task before implementation. Do not blindly replace all strings.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Draft Adapter Emits Canonical Builder Bindings
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_artifacts/drafts/adapter.py`
|
||||
- Modify: `tests/artifacts/test_draft_adapter.py`
|
||||
|
||||
- [ ] **Step 1: Add/adjust test**
|
||||
|
||||
Add a test proving a draft compiles through `WorkflowBuilder.use_ref(..., input=[...], output=[...])` or directly produces canonical `NodeUse.input` / `output`.
|
||||
|
||||
Expected assertion:
|
||||
|
||||
```python
|
||||
node = workflow.nodes[0]
|
||||
dumped = node.model_dump(mode="json")
|
||||
assert "in_map" not in dumped
|
||||
assert "out_map" not in dumped
|
||||
assert dumped["input"][0]["target"] == {"root": "local", "parts": ["text"]}
|
||||
assert dumped["input"][0]["path"] == {"root": "input", "parts": ["text"]}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update adapter**
|
||||
|
||||
Where it currently calls:
|
||||
|
||||
```python
|
||||
builder.use_ref(..., in_map=step.in_, input_values=..., out_map=step.out)
|
||||
```
|
||||
|
||||
convert draft structures into:
|
||||
|
||||
```python
|
||||
input=[...]
|
||||
output=[...]
|
||||
```
|
||||
|
||||
If draft models still store maps, transform them into canonical binding dicts at the adapter boundary.
|
||||
|
||||
- [ ] **Step 3: Run draft adapter tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/artifacts/test_draft_adapter.py -q
|
||||
```
|
||||
|
||||
Expected: pass and no new deprecation warnings from the adapter.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Workflow Surface Requests Prefer Canonical Shapes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
||||
- Modify: `tests/wf_mcp/test_workflow_surface.py`
|
||||
|
||||
- [ ] **Step 1: Add schema tests for canonical binding request fields**
|
||||
|
||||
Find the create/compile draft request model and assert its JSON Schema includes:
|
||||
|
||||
```json
|
||||
"input": {"type": "array", ...}
|
||||
"output": {"type": "array", ...}
|
||||
```
|
||||
|
||||
and does not force `in_map` / `out_map` as the primary example.
|
||||
|
||||
- [ ] **Step 2: Update Pydantic models**
|
||||
|
||||
Prefer these field names in MCP-facing request models:
|
||||
|
||||
```python
|
||||
input: list[InputBindingLike] = Field(default_factory=list, description=...)
|
||||
output: list[OutputBindingLike] = Field(default_factory=list, description=...)
|
||||
```
|
||||
|
||||
If compatibility maps remain:
|
||||
|
||||
```python
|
||||
in_map: dict[str, str] | None = Field(default=None, deprecated=True, description=...)
|
||||
out_map: dict[str, str] | None = Field(default=None, deprecated=True, description=...)
|
||||
```
|
||||
|
||||
If Pydantic `deprecated=True` causes schema issues, document deprecation in descriptions instead.
|
||||
|
||||
- [ ] **Step 3: Update handlers**
|
||||
|
||||
Handlers should pass canonical lists to builder/artifact APIs.
|
||||
|
||||
- [ ] **Step 4: Run workflow surface tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Inspect/List Outputs Show Canonical Refs and Display Strings Separately
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
||||
- Modify: `src/wf_platform/sources.py` if inventory models need fields
|
||||
- Modify: tests in `tests/wf_mcp`
|
||||
|
||||
- [ ] **Step 1: Add response-shape assertions**
|
||||
|
||||
For source/capability inspection responses, assert reducers include enough info:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "wf.std.add",
|
||||
"ref": {"source": "wf.std", "capability_key": "add"},
|
||||
"description": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Use `name` as display, `ref` as canonical.
|
||||
|
||||
- [ ] **Step 2: Update inventory models only if needed**
|
||||
|
||||
If `ReducerInventory` currently has only `name`, add:
|
||||
|
||||
```python
|
||||
ref: CapabilityRef
|
||||
```
|
||||
|
||||
or a serializable equivalent.
|
||||
|
||||
Keep old `name` for display.
|
||||
|
||||
- [ ] **Step 3: Run platform/MCP inventory tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/platform/test_inventory.py tests/wf_mcp/test_service.py tests/wf_mcp/test_workflow_surface.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Docs and MCP Tool Descriptions
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/wf_mcp_operator_manual.md`
|
||||
- Modify: `docs/workflow_drafts.md`
|
||||
- Modify: `docs/wf_mcp_end_to_end_runbook.md`
|
||||
- Modify: `docs/structural_refs.md`
|
||||
- Modify MCP tool descriptions in `src/wf_mcp/workflow_surface/tools.py` if needed
|
||||
|
||||
- [ ] **Step 1: Replace primary examples**
|
||||
|
||||
Replace examples that teach:
|
||||
|
||||
```json
|
||||
"in_map": {"input.text": "text"}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```json
|
||||
"input": [{"target": {"root": "local", "parts": ["text"]}, "path": {"root": "input", "parts": ["text"]}}]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Keep compatibility notes**
|
||||
|
||||
Add:
|
||||
|
||||
```text
|
||||
`in_map`, `input_values`, and `out_map` are compatibility inputs. New MCP/JSON
|
||||
clients should use `input` and `output` binding lists.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update reducer examples**
|
||||
|
||||
Show:
|
||||
|
||||
```json
|
||||
"reducer": "wf.std.add"
|
||||
```
|
||||
|
||||
for compact unconfigured reducers, and:
|
||||
|
||||
```json
|
||||
"reducer": {
|
||||
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
|
||||
"config": {"modulus": 10}
|
||||
}
|
||||
```
|
||||
|
||||
for configured reducers.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Verification
|
||||
|
||||
- [ ] **Step 1: Focused artifact/MCP tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/artifacts/test_draft_adapter.py tests/artifacts/test_draft_models.py tests/artifacts/test_draft_api.py tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_workflow_wrapper_hints.py -q
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Full tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest -q
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Static checks**
|
||||
|
||||
```bash
|
||||
uvx ruff check src/wf_mcp src/wf_artifacts tests/wf_mcp tests/artifacts
|
||||
uvx ruff format --check src/wf_mcp src/wf_artifacts tests/wf_mcp tests/artifacts
|
||||
uv run basedpyright --level error src/wf_mcp src/wf_artifacts tests/wf_mcp tests/artifacts
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- tests pass
|
||||
- ruff passes
|
||||
- basedpyright reports `0 errors`
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- MCP-facing examples prefer canonical binding lists.
|
||||
- Compatibility maps remain accepted where documented.
|
||||
- Draft adapter no longer emits deprecated builder sugar warnings.
|
||||
- Reducer refs display `name` and canonical `ref` distinctly where inventory exposes them.
|
||||
- No MCP handler reparses reducer dotted names by first/last dot.
|
||||
@@ -4,21 +4,36 @@
|
||||
|
||||
**Goal:** Move reducer references from ambiguous dotted strings toward structural capability refs while preserving string reducer names as parse-only shorthand.
|
||||
|
||||
**Architecture:** Reducers are source-owned capabilities, not graph paths. `ReducerRef` should carry a structural `CapabilityRef` plus config, while old `name` strings continue to validate at compatibility boundaries. Artifact dependency extraction should use the structural ref instead of reparsing dotted reducer names.
|
||||
**Architecture:** Reducers are source-owned capabilities, not graph paths. `ReducerRef` should carry a structural `CapabilityRef` plus config, while old `name` strings continue to validate at compatibility boundaries. Runtime reducer lookup can keep using display names temporarily through a compatibility property; artifact dependency extraction should stop reparsing dotted reducer names manually.
|
||||
|
||||
**Tech Stack:** Python 3.14, Pydantic v2, `wf_platform.refs.CapabilityRef`, `wf_core.models.reducers.ReducerRef`, `wf_artifacts.factory`, pytest, basedpyright, ruff.
|
||||
|
||||
---
|
||||
|
||||
## Planned Shape
|
||||
## Current State
|
||||
|
||||
Current compatibility shape:
|
||||
`src/wf_core/models/reducers.py`:
|
||||
|
||||
```json
|
||||
{"name": "wf.std.add", "config": {}}
|
||||
```python
|
||||
class ReducerRef(BaseModel):
|
||||
name: str
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
```
|
||||
|
||||
Future canonical shape:
|
||||
`src/wf_artifacts/factory.py` extracts reducer dependencies by reparsing the display name:
|
||||
|
||||
```python
|
||||
reducer_ref = CapabilityRef.parse(reducer.name)
|
||||
requirements[reducer.name] = RequiredCapability(ref=reducer_ref, kind="reducer")
|
||||
```
|
||||
|
||||
This is the same separator problem in another domain. `wf.std.add` is a capability ref, not a graph path.
|
||||
|
||||
---
|
||||
|
||||
## Canonical Shape
|
||||
|
||||
New canonical reducer ref:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -27,32 +42,386 @@ Future canonical shape:
|
||||
}
|
||||
```
|
||||
|
||||
String shorthand should continue to parse:
|
||||
Compatibility inputs:
|
||||
|
||||
```json
|
||||
"wf.std.add"
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```json
|
||||
{"name": "wf.std.add", "config": {"modulus": 10}}
|
||||
```
|
||||
|
||||
but saved model dumps should prefer `ref`.
|
||||
For now, `ReducerRef.name` remains available as a display/registry key compatibility property. Runtime reducer registries are still keyed by strings such as `wf.std.add`.
|
||||
|
||||
## Scope Notes
|
||||
---
|
||||
|
||||
- Do not treat reducer refs as `StatePath`.
|
||||
- Do not split reducer names using graph-path helpers.
|
||||
- Keep reducer config as part of `ReducerRef`; config does not affect the dependency key.
|
||||
- Update artifact dependency extraction to read `ReducerRef.ref`.
|
||||
- Keep runtime reducer lookup compatible with existing reducer registries keyed by display name until reducer catalogs are source-keyed.
|
||||
## File Structure
|
||||
|
||||
## First Implementation Tasks
|
||||
- Modify: `src/wf_core/models/reducers.py`
|
||||
- Add `ref: CapabilityRef`
|
||||
- Keep `name` as computed/display compatibility property
|
||||
- Parse old string and old `name` object shapes
|
||||
- Dump canonical `ref` shape in JSON/Python model dumps
|
||||
|
||||
1. Add tests for `ReducerRef.model_validate("wf.std.add")`.
|
||||
2. Add tests for canonical `{"ref": {"source": "wf.std", "capability_key": "add"}}`.
|
||||
3. Add a display-name compatibility property if runtime registries still use string keys.
|
||||
4. Update `_required_reducers_from_plan()` to use structural refs.
|
||||
5. Update docs and inventory output only after the model is stable.
|
||||
- Modify: `src/wf_artifacts/factory.py`
|
||||
- Use `reducer.ref` for required capabilities
|
||||
- Keep dependency key as `reducer.name` for now
|
||||
|
||||
- Modify tests:
|
||||
- `tests/core/test_nested_state_paths.py`
|
||||
- `tests/core/test_schema_validation.py`
|
||||
- `tests/artifacts/test_factory.py`
|
||||
- `tests/artifacts/test_validation.py` if needed
|
||||
|
||||
- Modify docs:
|
||||
- `docs/structural_refs.md`
|
||||
- `docs/core_state_mapping_and_merge.md`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Pin ReducerRef Compatibility and Canonical Dump
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/core/test_nested_state_paths.py`
|
||||
|
||||
- [ ] **Step 1: Add reducer ref tests**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
from wf_platform import CapabilityRef
|
||||
|
||||
|
||||
def test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref() -> None:
|
||||
reducer = ReducerRef.model_validate("wf.std.add")
|
||||
|
||||
assert reducer.ref == CapabilityRef(source="wf.std", capability_key="add")
|
||||
assert reducer.name == "wf.std.add"
|
||||
assert reducer.model_dump(mode="json") == {
|
||||
"ref": {"source": "wf.std", "capability_key": "add"},
|
||||
"config": {},
|
||||
}
|
||||
|
||||
|
||||
def test_reducer_ref_accepts_legacy_name_object_with_config() -> None:
|
||||
reducer = ReducerRef.model_validate({
|
||||
"name": "wf.std.modulo_add",
|
||||
"config": {"modulus": 10},
|
||||
})
|
||||
|
||||
assert reducer.ref == CapabilityRef(source="wf.std", capability_key="modulo_add")
|
||||
assert reducer.name == "wf.std.modulo_add"
|
||||
assert reducer.config == {"modulus": 10}
|
||||
|
||||
|
||||
def test_reducer_ref_accepts_canonical_ref_object() -> None:
|
||||
reducer = ReducerRef.model_validate({
|
||||
"ref": {"source": "wf.std", "capability_key": "append"},
|
||||
})
|
||||
|
||||
assert reducer.name == "wf.std.append"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused tests to verify red**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_legacy_name_object_with_config tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_canonical_ref_object -q
|
||||
```
|
||||
|
||||
Expected: fail because `ReducerRef` does not parse strings and has no `ref`.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Implement Structural ReducerRef
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_core/models/reducers.py`
|
||||
|
||||
- [ ] **Step 1: Update imports**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
from collections.abc import Mapping
|
||||
from pydantic import computed_field, model_validator
|
||||
from wf_platform import CapabilityRef
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Change model fields**
|
||||
|
||||
Change `ReducerRef` to:
|
||||
|
||||
```python
|
||||
class ReducerRef(BaseModel):
|
||||
"""Reference to one reducer capability plus JSON-compatible configuration."""
|
||||
|
||||
ref: CapabilityRef
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add compatibility validator**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _coerce_legacy_shapes(cls, value: object) -> object:
|
||||
if isinstance(value, str):
|
||||
return {"ref": CapabilityRef.parse(value)}
|
||||
if not isinstance(value, Mapping):
|
||||
return value
|
||||
data = dict(value)
|
||||
if "ref" not in data and "name" in data:
|
||||
data["ref"] = CapabilityRef.parse(str(data.pop("name")))
|
||||
return data
|
||||
```
|
||||
|
||||
Do not parse arbitrary dotted strings anywhere else.
|
||||
|
||||
- [ ] **Step 4: Add name compatibility property**
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
@computed_field
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Display/registry compatibility key for existing reducer catalogs."""
|
||||
return str(self.ref)
|
||||
```
|
||||
|
||||
If `CapabilityRef.__str__` does not produce `source.capability_key`, use its display helper or add one there.
|
||||
|
||||
- [ ] **Step 5: Run reducer ref tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_string_shorthand_and_dumps_structural_ref tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_legacy_name_object_with_config tests/core/test_nested_state_paths.py::test_reducer_ref_accepts_canonical_ref_object -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Update Reducer Field Serializers and Existing Expectations
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_core/models/schemas.py`
|
||||
- Modify tests that assert reducer dumps
|
||||
|
||||
- [ ] **Step 1: Inspect current reducer dump helper**
|
||||
|
||||
Current helper:
|
||||
|
||||
```python
|
||||
def _dump_reducer_keyword(reducer: ReducerRef) -> str | dict[str, Any]:
|
||||
if not reducer.config:
|
||||
return reducer.name
|
||||
return reducer.model_dump(mode="json")
|
||||
```
|
||||
|
||||
Decide canonical output:
|
||||
|
||||
- For no-config reducers, keep string shorthand in JSON Schema `reducer` keyword for readability.
|
||||
- For configured reducers, dump canonical object:
|
||||
|
||||
```json
|
||||
{
|
||||
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
|
||||
"config": {"modulus": 10}
|
||||
}
|
||||
```
|
||||
|
||||
This keeps common schema compact while avoiding string parsing for config objects.
|
||||
|
||||
- [ ] **Step 2: Update helper**
|
||||
|
||||
Use:
|
||||
|
||||
```python
|
||||
def _dump_reducer_keyword(reducer: ReducerRef) -> str | dict[str, Any]:
|
||||
if not reducer.config:
|
||||
return reducer.name
|
||||
return reducer.model_dump(mode="json")
|
||||
```
|
||||
|
||||
This may already work after `ReducerRef.model_dump()` changes. Keep the helper but update tests.
|
||||
|
||||
- [ ] **Step 3: Run schema tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/core/test_schema_validation.py tests/core/test_nested_state_paths.py -q
|
||||
```
|
||||
|
||||
Expected: pass after updating expectations for configured reducer dumps if needed.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update Artifact Reducer Dependency Extraction
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_artifacts/factory.py`
|
||||
- Modify: `tests/artifacts/test_factory.py`
|
||||
|
||||
- [ ] **Step 1: Add/adjust artifact test**
|
||||
|
||||
In `tests/artifacts/test_factory.py`, ensure reducer dependencies assert structural refs:
|
||||
|
||||
```python
|
||||
def test_create_workflow_artifact_from_plan_adds_reducer_dependencies() -> None:
|
||||
...
|
||||
reducer = artifact.required_capability_map()["wf.std.max"]
|
||||
assert reducer.ref.source == "wf.std"
|
||||
assert reducer.ref.capability_key == "max"
|
||||
assert reducer.logical_source == "wf.std"
|
||||
assert reducer.capability_name == "max"
|
||||
assert reducer.kind == "reducer"
|
||||
```
|
||||
|
||||
Add a configured reducer payload test:
|
||||
|
||||
```python
|
||||
def test_create_workflow_artifact_from_plan_accepts_structural_reducer_ref() -> None:
|
||||
plan = minimal_plan()
|
||||
plan["state_schema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer",
|
||||
"reducer": {
|
||||
"ref": {"source": "wf.std", "capability_key": "max"},
|
||||
"config": {},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
artifact = create_workflow_artifact_from_plan(...)
|
||||
|
||||
assert "wf.std.max" in artifact.required_capability_map()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update extraction**
|
||||
|
||||
Change:
|
||||
|
||||
```python
|
||||
reducer_ref = CapabilityRef.parse(reducer.name)
|
||||
requirements[reducer.name] = RequiredCapability(ref=reducer_ref, kind="reducer")
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
requirements[reducer.name] = RequiredCapability(ref=reducer.ref, kind="reducer")
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run artifact tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/artifacts/test_factory.py tests/artifacts/test_validation.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Runtime Compatibility Check
|
||||
|
||||
**Files:**
|
||||
- Tests only unless failures require runtime changes
|
||||
|
||||
- [ ] **Step 1: Run reducer runtime tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/core/test_nested_state_paths.py tests/core/test_atomic_state_patches.py -q
|
||||
```
|
||||
|
||||
Expected: pass because `reducer.name` remains a compatibility registry key.
|
||||
|
||||
- [ ] **Step 2: If runtime fails**
|
||||
|
||||
Only if needed, update lookup code to use `reducer.name` as the compatibility string key. Do not make runtime registries structural in this pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/structural_refs.md`
|
||||
- Modify: `docs/core_state_mapping_and_merge.md`
|
||||
|
||||
- [ ] **Step 1: Update reducer docs**
|
||||
|
||||
In `docs/structural_refs.md`, replace temporary wording with:
|
||||
|
||||
```text
|
||||
Canonical configured reducer refs use `ref`:
|
||||
|
||||
{
|
||||
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
|
||||
"config": {"modulus": 10}
|
||||
}
|
||||
|
||||
String reducer names such as `wf.std.add` remain shorthand for unconfigured
|
||||
reducers and compatibility display.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update merge docs**
|
||||
|
||||
In `docs/core_state_mapping_and_merge.md`, update examples with both compact and configured forms:
|
||||
|
||||
```json
|
||||
"reducer": "wf.std.add"
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```json
|
||||
"reducer": {
|
||||
"ref": {"source": "wf.std", "capability_key": "modulo_add"},
|
||||
"config": {"modulus": 10}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Verification
|
||||
|
||||
- [ ] **Step 1: Focused tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest tests/core/test_nested_state_paths.py tests/core/test_schema_validation.py tests/artifacts/test_factory.py tests/artifacts/test_validation.py -q
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Full tests**
|
||||
|
||||
```bash
|
||||
uv run --with pytest pytest -q
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Static checks**
|
||||
|
||||
```bash
|
||||
uvx ruff check src/wf_core src/wf_artifacts tests/core tests/artifacts
|
||||
uvx ruff format --check src/wf_core src/wf_artifacts tests/core tests/artifacts
|
||||
uv run basedpyright --level error src/wf_core src/wf_artifacts tests/core tests/artifacts
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- tests pass
|
||||
- ruff passes
|
||||
- basedpyright reports `0 errors`
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- `ReducerRef` canonical shape has `ref`, not only `name`.
|
||||
- String shorthand still parses.
|
||||
- Legacy `{name, config}` still parses.
|
||||
- Runtime reducer lookup still works through `reducer.name`.
|
||||
- Artifact dependency extraction uses `reducer.ref`.
|
||||
- No graph path parser is used for reducer refs.
|
||||
|
||||
Reference in New Issue
Block a user