434 lines
11 KiB
Markdown
434 lines
11 KiB
Markdown
# ReducerRef Structural Capability 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 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. 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.
|
|
|
|
---
|
|
|
|
## Current State
|
|
|
|
`src/wf_core/models/reducers.py`:
|
|
|
|
```python
|
|
class ReducerRef(BaseModel):
|
|
name: str
|
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
```
|
|
|
|
`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
|
|
{
|
|
"ref": { "source": "wf.std", "capability_key": "add" },
|
|
"config": {}
|
|
}
|
|
```
|
|
|
|
Compatibility inputs:
|
|
|
|
```json
|
|
"wf.std.add"
|
|
```
|
|
|
|
```json
|
|
{ "name": "wf.std.add", "config": { "modulus": 10 } }
|
|
```
|
|
|
|
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`.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- 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
|
|
|
|
- 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.
|