18 KiB
Structural Capability Refs 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: Stop treating dotted capability names as authoritative data by making source/capability/workflow refs structural in saved artifacts and internal runtime paths.
Architecture: Public MCP/tool arguments may keep accepting old string names for compatibility, but model validation normalizes them into structural refs immediately. Saved artifacts, deployments, and generated JSON should write structured fields; display strings are derived only for UI/tool readability. Runtime resolution binds by explicit source fields, not by guessing where a dot-separated string should split.
Tech Stack: Python 3.14, Pydantic models, wf_platform refs, wf_artifacts models, wf_mcp workflow surface and broker service tests.
Current Problem
Dotted names currently carry multiple meanings:
context7.default.query-docsmeans provider/profile/capability.wf.std.replacemeans system source/capability.workflow.echo_wrapper.v1means workflow artifact/version.demo.foo.barmay mean sourcedemowith capability keyfoo.bar, or sourcedemo.foowith capability keybar.
No parser can infer the right boundary from the string alone. Dotted strings are still useful as display names and human-entered compatibility inputs, but they must not be the stored source of truth.
Canonical Shapes
Source Identity
Use SourceRef for concrete or logical source identity.
{
"provider": "context7",
"profile": "default"
}
System sources may omit profile:
{
"provider": "wf.std",
"profile": null
}
For the first implementation pass, this can still serialize through the current SourceRef string internally, but the plan should leave room for profile: null.
Capability Ref
Use an object with a source and a local key:
{
"source": "demo",
"capability_key": "foo.bar"
}
capability_key is the name inside a known source. It may contain dots. It is not parsed for source meaning.
Workflow Artifact Capability Ref
Workflow artifacts are not external source capabilities. Store them separately:
{
"artifact_id": "echo_wrapper",
"version": 1
}
The display string workflow.echo_wrapper.v1 remains computable for list output and old input parsing.
File Structure
-
Modify
src/wf_platform/refs.py- Add structural capability ref input/output support.
- Keep compact-string parsing for old inputs.
- Make JSON serialization emit object shape for canonical saves.
-
Modify
src/wf_artifacts/refs.py- Add Pydantic validation/serialization for
WorkflowCapabilityRef. - Accept old
workflow.<artifact_id>.v<version>strings as parse-only input. - Serialize new saves as
{"artifact_id": "...", "version": 1}.
- Add Pydantic validation/serialization for
-
Modify
src/wf_artifacts/models.py- Make
RequiredCapability.refcanonical structural JSON. - Keep
logical_sourceandcapability_nameas compatibility accessors only. - Keep accepting old dict/map/string shapes.
- Make
WorkflowDeployment.bindingssave as structural list, not dict.
- Make
-
Modify
src/wf_artifacts/references.py- Replace string-concatenated
logical_refconstruction with structuralCapabilityRef. - Return display strings only where legacy maps still require them.
- Replace string-concatenated
-
Modify
src/wf_mcp/workflow_surface/refs.py- Parse workflow-surface ids into a union of structural refs.
- Keep old string input compatibility for MCP tool calls.
-
Modify
src/wf_mcp/workflow_surface/runtime_dependencies.py- Keep source-prefix binding logic for legacy plan node strings.
- Add a path for structural plan node refs when the plan model supports them.
- Keep the current dotted-local-name regression test.
-
Modify
src/wf_mcp/workflow_surface/handlers.py- When saving artifacts from plans/drafts/workspaces, emit structural required capability refs.
- Keep response display names for humans/LLMs.
-
Add or modify tests:
tests/wf_platform/test_refs.pytests/wf_artifacts/test_refs.pytests/wf_mcp/test_workflow_surface_refs.pytests/wf_mcp/test_service.pytests/wf_mcp/test_workflow_surface.py
Task 1: Make CapabilityRef Serialize Structurally
Files:
-
Modify:
src/wf_platform/refs.py -
Test:
tests/wf_platform/test_refs.py -
Step 1: Write failing tests
Add tests that prove three things:
from pydantic import BaseModel
from wf_platform import CapabilityRef, SourceRef
class RefHolder(BaseModel):
ref: CapabilityRef
def test_capability_ref_accepts_legacy_string_input() -> None:
holder = RefHolder.model_validate({"ref": "demo.foo.bar"})
# Legacy parsing is best-effort only. It stays for old input compatibility.
assert str(holder.ref) == "demo.foo.bar"
def test_capability_ref_accepts_structural_input() -> None:
holder = RefHolder.model_validate(
{"ref": {"source": "demo", "capability_key": "foo.bar"}}
)
assert holder.ref.source == SourceRef.parse("demo")
assert holder.ref.name == "foo.bar"
def test_capability_ref_serializes_structurally() -> None:
holder = RefHolder(
ref=CapabilityRef(source=SourceRef.parse("demo"), name="foo.bar")
)
assert holder.model_dump(mode="json")["ref"] == {
"source": "demo",
"capability_key": "foo.bar",
}
- Step 2: Run tests to verify red
Run:
uv run --with pytest pytest tests/wf_platform/test_refs.py -q
Expected: structural input or structural serialization fails because CapabilityRef currently serializes as a string.
- Step 3: Implement structural validation/serialization
Update CapabilityRef.__get_pydantic_core_schema__ to:
- accept existing
CapabilityRef - accept legacy string via
CapabilityRef.parse - accept dict
{"source": str, "capability_key": str} - serialize as dict
{"source": str(self.source), "capability_key": self.name}
Keep __str__ unchanged because display strings are still useful.
- Step 4: Run tests to verify green
Run:
uv run --with pytest pytest tests/wf_platform/test_refs.py -q
Expected: all tests pass.
Task 2: Make WorkflowCapabilityRef Structural
Files:
-
Modify:
src/wf_artifacts/refs.py -
Test:
tests/wf_artifacts/test_refs.py -
Step 1: Write failing tests
from pydantic import BaseModel
from wf_artifacts import WorkflowCapabilityRef
class WorkflowRefHolder(BaseModel):
ref: WorkflowCapabilityRef
def test_workflow_capability_ref_accepts_legacy_string_input() -> None:
holder = WorkflowRefHolder.model_validate({"ref": "workflow.echo_wrapper.v1"})
assert holder.ref.artifact_id == "echo_wrapper"
assert holder.ref.version == 1
def test_workflow_capability_ref_accepts_structural_input() -> None:
holder = WorkflowRefHolder.model_validate(
{"ref": {"artifact_id": "echo_wrapper", "version": 1}}
)
assert holder.ref.artifact_id == "echo_wrapper"
assert holder.ref.version == 1
def test_workflow_capability_ref_serializes_structurally() -> None:
holder = WorkflowRefHolder(ref=WorkflowCapabilityRef("echo_wrapper", 1))
assert holder.model_dump(mode="json")["ref"] == {
"artifact_id": "echo_wrapper",
"version": 1,
}
- Step 2: Run tests to verify red
Run:
uv run --with pytest pytest tests/wf_artifacts/test_refs.py -q
Expected: Pydantic validation fails because WorkflowCapabilityRef has no schema hook.
- Step 3: Implement Pydantic schema hook
Add a Pydantic core schema method to WorkflowCapabilityRef that accepts legacy string and structural dict input, then serializes structurally.
- Step 4: Run tests to verify green
Run:
uv run --with pytest pytest tests/wf_artifacts/test_refs.py -q
Expected: all tests pass.
Task 3: Save Required Capabilities in New Shape
Files:
-
Modify:
src/wf_artifacts/models.py -
Modify:
src/wf_artifacts/references.py -
Test:
tests/wf_artifacts/test_models.py -
Test: existing workflow-surface artifact creation tests
-
Step 1: Write failing model serialization tests
Add tests proving old inputs parse and new outputs dump structurally:
from wf_artifacts import RequiredCapability, WorkflowArtifact
def test_required_capability_accepts_legacy_logical_fields_but_dumps_ref_object() -> None:
capability = RequiredCapability.model_validate(
{
"logical_source": "demo",
"capability_name": "foo.bar",
"kind": "node_spec",
}
)
assert capability.logical_source == "demo"
assert capability.capability_name == "foo.bar"
assert capability.model_dump(mode="json")["ref"] == {
"source": "demo",
"capability_key": "foo.bar",
}
def test_workflow_artifact_accepts_legacy_required_capability_map_but_dumps_list() -> None:
artifact = WorkflowArtifact.model_validate(
{
"id": "echo",
"version": 1,
"title": "Echo",
"input_schema": {"type": "object", "properties": {}},
"output_schema": {"type": "object", "properties": {}},
"outcomes": ["completed"],
"plan": {"name": "echo", "nodes": [], "edges": []},
"required_capabilities": {
"demo.foo.bar": {"kind": "node_spec"},
},
}
)
dumped = artifact.model_dump(mode="json")
assert dumped["required_capabilities"][0]["ref"] == {
"source": "demo.foo",
"capability_key": "bar",
}
This test documents legacy string parsing as best-effort. New artifact creation should avoid this path when source bindings are known.
- Step 2: Run tests to verify red
Run:
uv run --with pytest pytest tests/wf_artifacts/test_models.py -q
Expected: dumps still contain old string refs.
- Step 3: Update model serialization
After Task 1, RequiredCapability.ref should dump structurally automatically. Ensure WorkflowArtifact._reject_duplicate_required_capabilities still works by using str(capability.capability_ref()) only for internal duplicate checking.
- Step 4: Update reference creation
In src/wf_artifacts/references.py, replace this conceptual behavior:
logical_ref = "demo.foo.bar"
RequiredCapability(ref=CapabilityRef.parse(logical_ref), ...)
with structural construction:
capability_ref = CapabilityRef(
source=SourceRef.parse(logical_source),
name=capability_name,
)
RequiredCapability(ref=capability_ref, ...)
Only derive str(capability_ref) for compatibility map keys.
- Step 5: Run tests to verify green
Run:
uv run --with pytest pytest tests/wf_artifacts/test_models.py tests/wf_mcp/test_workflow_surface.py -q
Expected: artifact creation still works; saved dumps use structural refs.
Task 4: Make Deployment Bindings Structural on Save
Files:
-
Modify:
src/wf_artifacts/models.py -
Test:
tests/wf_artifacts/test_models.py -
Test:
tests/wf_mcp/test_workflow_surface.py -
Step 1: Write failing binding serialization test
from wf_artifacts import WorkflowDeployment
def test_workflow_deployment_accepts_legacy_binding_map_but_dumps_structural_list() -> None:
deployment = WorkflowDeployment.model_validate(
{
"id": "echo.personal",
"artifact_id": "echo",
"artifact_version": 1,
"bindings": {"demo": "demo.personal", "wf.std": "wf.std"},
}
)
dumped = deployment.model_dump(mode="json")
assert dumped["bindings"] == [
{"logical_source": "demo", "concrete_source": "demo.personal"},
{"logical_source": "wf.std", "concrete_source": "wf.std"},
]
- Step 2: Run tests to verify red
Run:
uv run --with pytest pytest tests/wf_artifacts/test_models.py -q
Expected: output may already be a list, but confirm SourceRef serialization remains stable.
- Step 3: Keep current binding shape but document it as structural
SourceBinding already separates logical and concrete source fields. Add docstrings explaining:
-
logical_sourceis an artifact-local alias. -
concrete_sourceis the deployment-selected source id. -
neither field is a capability name.
-
Step 4: Run tests
Run:
uv run --with pytest pytest tests/wf_artifacts/test_models.py tests/wf_mcp/test_workflow_surface.py -q
Expected: all tests pass.
Task 5: Stop Parsing Workflow Capability Strings as Generic Capabilities
Files:
-
Modify:
src/wf_mcp/workflow_surface/refs.py -
Modify:
src/wf_mcp/workflow_surface/handlers.py -
Test:
tests/wf_mcp/test_workflow_surface_refs.py -
Test:
tests/wf_mcp/test_workflow_surface.py -
Step 1: Write tests for separate ref domains
from wf_artifacts import WorkflowCapabilityRef
from wf_mcp.workflow_surface.refs import parse_workflow_surface_capability_id
from wf_platform import CapabilityRef
def test_workflow_surface_ref_parser_keeps_workflow_artifacts_separate() -> None:
parsed = parse_workflow_surface_capability_id("workflow.echo_wrapper.v1")
assert isinstance(parsed, WorkflowCapabilityRef)
assert parsed.artifact_id == "echo_wrapper"
assert parsed.version == 1
def test_workflow_surface_ref_parser_keeps_source_capabilities_structural() -> None:
parsed = parse_workflow_surface_capability_id(
{"source": "demo", "capability_key": "foo.bar"}
)
assert isinstance(parsed, CapabilityRef)
assert str(parsed.source) == "demo"
assert parsed.name == "foo.bar"
- Step 2: Run tests to verify red
Run:
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface_refs.py -q
Expected: dict input fails because parser currently accepts strings only.
- Step 3: Update parser input type
Allow parser input as:
str | dict[str, object]
Rules:
-
if string starts with
workflow., tryWorkflowCapabilityRef.parse -
if dict has
artifact_idandversion, parse as workflow artifact ref -
otherwise parse as
CapabilityRef -
Step 4: Run workflow surface tests
Run:
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface_refs.py tests/wf_mcp/test_workflow_surface.py -q
Expected: all tests pass.
Task 6: Keep Runtime Binding Source-Aware
Files:
-
Modify:
src/wf_mcp/workflow_surface/runtime_dependencies.py -
Test:
tests/wf_mcp/test_service.py -
Step 1: Keep the dotted-local-name regression
Ensure this test remains:
def test_service_runs_logical_source_plan_with_dotted_local_name() -> None:
...
plan = _single_echo_plan("logical_dotted_local_name_plan", "demo.foo.bar")
...
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}]
...
assert run.output["echoed"] == "hello"
- Step 2: Add a unit test for longest binding prefix
Add a focused test through public runtime behavior where both demo and demo.pro are bound, and demo.pro.foo.bar resolves through demo.pro.
- Step 3: Keep
_bound_node_namesor move it to a shared helper
If more than one module needs source-prefix binding, move the helper to a small module such as:
src/wf_artifacts/bindings.py
Do not move it preemptively if runtime remains the only caller.
- Step 4: Run tests
Run:
uv run --with pytest pytest tests/wf_mcp/test_service.py -q
Expected: all tests pass.
Task 7: Update Docs to State the Rule
Files:
-
Modify:
docs/workflow_capabilities.md -
Create or modify:
docs/structural_refs.md -
Step 1: Add the core rule
Add this rule prominently:
Qualified names are display strings. They are not authoritative identifiers.
Saved workflow artifacts and deployments should store structural refs.
Old strings are accepted at API boundaries only for compatibility.
- Step 2: Add examples
Include examples for:
{ "source": "demo", "capability_key": "foo.bar" }
{ "artifact_id": "echo_wrapper", "version": 1 }
{ "logical_source": "demo", "concrete_source": "demo.personal" }
- Step 3: Mention path refs are separate
Add:
Capability refs and graph paths are different domains. Path strings such as
state.person.name should migrate separately to path models.
Task 8: Verification
Files:
-
All touched files.
-
Step 1: Run focused tests
Run:
uv run --with pytest pytest tests/wf_platform tests/wf_artifacts tests/wf_mcp/test_service.py tests/wf_mcp/test_workflow_surface.py -q
Expected: all selected tests pass.
- Step 2: Run full tests when workspace is stable
Run:
uv run --with pytest pytest -q
Expected: full suite passes, except any explicitly user-owned temporary rewrite tests if the user says to ignore them.
- Step 3: Run linters/type checker
Run:
uvx ruff check
uv run basedpyright --level error
Expected: no new errors.
Self-Review Notes
- This plan does not require changing all workflow plan node refs in one pass. Runtime accepts legacy plan strings while artifact metadata becomes structural first.
- This plan does not solve graph path ambiguity. Graph paths need their own migration to
GraphPath/LocalPath. - This plan does not force
profileinto every model immediately. It keepsSourceRefcompatible and documentsprofileas future concrete source structure. - This plan keeps MCP/client compatibility by accepting old string inputs and continuing to display derived names.