Files
lda-wf/docs/historical/superpowers/plans/2026-05-21-reducer-ref-structural-capability.md
T

11 KiB

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:

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:

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:

{
  "ref": { "source": "wf.std", "capability_key": "add" },
  "config": {}
}

Compatibility inputs:

"wf.std.add"
{ "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:

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
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:

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:

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:

@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:

@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
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:

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:
{
  "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:

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
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:

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:

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:

reducer_ref = CapabilityRef.parse(reducer.name)
requirements[reducer.name] = RequiredCapability(ref=reducer_ref, kind="reducer")

to:

requirements[reducer.name] = RequiredCapability(ref=reducer.ref, kind="reducer")
  • Step 3: Run artifact tests
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

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:

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:

"reducer": "wf.std.add"

and:

"reducer": {
  "ref": {"source": "wf.std", "capability_key": "modulo_add"},
  "config": {"modulus": 10}
}

Task 7: Verification

  • Step 1: Focused tests
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
uv run --with pytest pytest -q
  • Step 3: Static checks
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.