docs: plan semantic draft authoring
This commit is contained in:
@@ -0,0 +1,311 @@
|
|||||||
|
# Canonical TOML Path Strings 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 canonical TOML-key strings the advertised and serialized representation for workflow paths while retaining structured-object input compatibility for existing stored data.
|
||||||
|
|
||||||
|
**Architecture:** Keep `GraphSourcePath`, `StatePath`, and `LocalPath` as structured immutable values. Move TOML-key parsing into `wf_core.paths`, add one canonical formatter there, and make every Pydantic path serializer emit strings. `wf_authoring` delegates to the core parser instead of owning a second grammar.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, stdlib `tomllib`, Pydantic v2 core schemas, pytest, Ruff, basedpyright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Define The Core TOML-Key Grammar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_core/paths.py`
|
||||||
|
- Test: `tests/core/test_path_values.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing parser and formatter tests**
|
||||||
|
|
||||||
|
Add tests proving quoted segments round-trip and roots remain typed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_toml_path_strings_round_trip_literal_segments() -> None:
|
||||||
|
source = GraphSourcePath.parse('input."customer.name"."display name"')
|
||||||
|
target = StatePath.parse('state."report.title"')
|
||||||
|
local = LocalPath.parse('payload."raw.value"')
|
||||||
|
|
||||||
|
assert source.parts == ("customer.name", "display name")
|
||||||
|
assert target.parts == ("report.title",)
|
||||||
|
assert local.parts == ("payload", "raw.value")
|
||||||
|
assert str(source) == 'input."customer.name"."display name"'
|
||||||
|
assert str(target) == 'state."report.title"'
|
||||||
|
assert str(local) == 'payload."raw.value"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Also test `LocalPath.parse(".")`, bare keys, malformed TOML, invalid roots, and empty writable state paths.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused test and verify RED**
|
||||||
|
|
||||||
|
Run: `uv run pytest tests/core/test_path_values.py -q`
|
||||||
|
|
||||||
|
Expected: quoted full paths fail because current core parsing uses `str.split(".")`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement shared parsing and formatting**
|
||||||
|
|
||||||
|
In `src/wf_core/paths.py`, add the shared implementation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_BARE_TOML_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_toml_path_segments(expr: str) -> tuple[str, ...]:
|
||||||
|
"""Parse a TOML key expression into literal path segments."""
|
||||||
|
try:
|
||||||
|
parsed = tomllib.loads(f"{expr} = true")
|
||||||
|
except tomllib.TOMLDecodeError as exc:
|
||||||
|
raise PathResolutionError(
|
||||||
|
f"invalid TOML path {expr!r}; quote path segments containing dots or spaces"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
current: object = parsed
|
||||||
|
while isinstance(current, dict):
|
||||||
|
if len(current) != 1:
|
||||||
|
raise PathResolutionError(f"invalid TOML path {expr!r}")
|
||||||
|
key, current = next(iter(current.items()))
|
||||||
|
parts.append(_validate_segment(key, path_kind="TOML path"))
|
||||||
|
if current is not True or not parts:
|
||||||
|
raise PathResolutionError(f"invalid TOML path {expr!r}")
|
||||||
|
return tuple(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def format_toml_path_segments(parts: tuple[str, ...]) -> str:
|
||||||
|
"""Format literal segments as one canonical TOML key expression."""
|
||||||
|
if not parts:
|
||||||
|
raise PathResolutionError("cannot format an empty TOML path")
|
||||||
|
return ".".join(
|
||||||
|
part
|
||||||
|
if _BARE_TOML_KEY.fullmatch(part)
|
||||||
|
else json.dumps(part, ensure_ascii=False)
|
||||||
|
for part in parts
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `tomllib.loads(f"{expr} = true")` for parsing. The formatter emits bare
|
||||||
|
TOML keys when legal and quoted TOML basic strings otherwise. Keep the local
|
||||||
|
root marker `.` as a special complete path, not a TOML key expression.
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
```python
|
||||||
|
GraphSourcePath.parse(raw)
|
||||||
|
GraphSourcePath.__str__()
|
||||||
|
StatePath.parse(raw)
|
||||||
|
StatePath.__str__()
|
||||||
|
LocalPath.parse(raw)
|
||||||
|
LocalPath.__str__()
|
||||||
|
```
|
||||||
|
|
||||||
|
`GraphSourcePath.parse` parses the whole expression, then treats the first
|
||||||
|
segment as the graph root. `StatePath` requires root `state` plus at least one
|
||||||
|
remaining segment. `LocalPath` has no serialized `local.` prefix.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused core tests**
|
||||||
|
|
||||||
|
Run: `uv run pytest tests/core/test_path_values.py tests/core/test_nested_state_paths.py -q`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/wf_core/paths.py tests/core/test_path_values.py
|
||||||
|
git commit -m "feat: define canonical TOML workflow paths"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Serialize Paths As Strings And Advertise Strings In JSON Schema
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_core/paths.py`
|
||||||
|
- Modify: `tests/core/test_path_values.py`
|
||||||
|
- Modify: `tests/core/test_canonical_node_bindings.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing Pydantic projection tests**
|
||||||
|
|
||||||
|
Extend the existing Pydantic path payload test:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_path_models_serialize_strings_but_accept_structural_compat() -> None:
|
||||||
|
payload = PathPayload.model_validate(
|
||||||
|
{
|
||||||
|
"source": {"root": "input", "parts": ["user.name"]},
|
||||||
|
"target": {"root": "state", "parts": ["person name"]},
|
||||||
|
"local": {"root": "local", "parts": ["payload.text"]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload.model_dump(mode="json") == {
|
||||||
|
"source": 'input."user.name"',
|
||||||
|
"target": 'state."person name"',
|
||||||
|
"local": '"payload.text"',
|
||||||
|
}
|
||||||
|
assert PathPayload.model_json_schema()["properties"]["source"]["type"] == "string"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests and verify RED**
|
||||||
|
|
||||||
|
Run: `uv run pytest tests/core/test_path_values.py tests/core/test_canonical_node_bindings.py -q`
|
||||||
|
|
||||||
|
Expected: current serializers emit `{root, parts}` objects.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Change serializers and JSON schemas**
|
||||||
|
|
||||||
|
Make each path `_serialize` return `str(value)`. Replace the structural
|
||||||
|
`_path_json_schema` with a string schema whose description documents TOML-key
|
||||||
|
quoting. Do not include the compatibility object in generated JSON Schema;
|
||||||
|
validators continue accepting it so old persisted records remain readable.
|
||||||
|
|
||||||
|
Keep a comment at the validator seam:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Structural objects remain input-only compatibility for persisted records.
|
||||||
|
# New schemas and serializers expose the canonical TOML-key string form.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update exact serialized binding expectations**
|
||||||
|
|
||||||
|
Adjust tests that assert JSON payloads to expect strings such as
|
||||||
|
`input.message`, `state.echoed`, and `.`. Keep field-level assertions rather
|
||||||
|
than replacing whole large snapshots.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run core serialization tests**
|
||||||
|
|
||||||
|
Run: `uv run pytest tests/core/test_path_values.py tests/core/test_canonical_node_bindings.py tests/core/test_run_codec.py -q`
|
||||||
|
|
||||||
|
Expected: PASS, including decoding old structural path objects.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/wf_core/paths.py tests/core/test_path_values.py tests/core/test_canonical_node_bindings.py
|
||||||
|
git commit -m "feat: serialize canonical workflow path strings"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Remove The Duplicate Authoring Parser
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_authoring/dsl/path_inputs.py`
|
||||||
|
- Modify: `tests/authoring/test_path_inputs.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add delegation coverage**
|
||||||
|
|
||||||
|
Add a test proving full rooted strings and explicit-root expressions resolve to
|
||||||
|
the same structured value:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_authoring_paths_share_core_toml_grammar() -> None:
|
||||||
|
assert coerce_graph_path('state."person.name"') == GraphSourcePath(
|
||||||
|
"state", ("person.name",)
|
||||||
|
)
|
||||||
|
assert coerce_graph_path('"person.name"', root="state") == GraphSourcePath(
|
||||||
|
"state", ("person.name",)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace `_parse_toml_key_expr`**
|
||||||
|
|
||||||
|
Delete the local `tomllib` parser and import
|
||||||
|
`parse_toml_path_segments` from `wf_core.paths`. Preserve iterable and
|
||||||
|
structural-object coercion behavior for Python callers.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run authoring tests**
|
||||||
|
|
||||||
|
Run: `uv run pytest tests/authoring/test_path_inputs.py tests/authoring/test_builder.py tests/authoring/test_conditions.py -q`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/wf_authoring/dsl/path_inputs.py tests/authoring/test_path_inputs.py
|
||||||
|
git commit -m "refactor: share workflow path grammar"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Update Draft, Transport, And Compatibility Coverage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/wf_api/test_drafts_service.py`
|
||||||
|
- Modify: `tests/wf_transport_rpc_http/test_app.py`
|
||||||
|
- Modify: `tests/wf_transport_rpc_http/test_client.py`
|
||||||
|
- Modify: `tests/wf_mcp/workflow_surface/test_drafts.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add one stored-draft compatibility regression**
|
||||||
|
|
||||||
|
Create a workspace from a draft containing structural path objects, retrieve or
|
||||||
|
patch it, and assert the next serialized draft uses canonical strings while
|
||||||
|
preserving the same path values.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update focused transport assertions**
|
||||||
|
|
||||||
|
Change only assertions for serialized path fields. RPC and MCP requests should
|
||||||
|
advertise and return strings; input model tests must still accept structural
|
||||||
|
objects.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run affected suites**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_mcp/workflow_surface/test_drafts.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_mcp/workflow_surface/test_drafts.py
|
||||||
|
git commit -m "test: cover canonical path transport compatibility"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Documentation And Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/workflow_drafts.md`
|
||||||
|
- Modify: `docs/wf_authoring_control_flow.md`
|
||||||
|
- Modify: `skills/wf-cli/SKILL.md`
|
||||||
|
- Modify: `skills/wf-workflow/references/draft-workspaces.md`
|
||||||
|
- Modify: `skills/wf-workflow/references/direct-plan-import.md`
|
||||||
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
- Modify: `docs/superpowers/specs/2026-06-27-draft-semantic-authoring-boundary.md`
|
||||||
|
- Move after completion: `docs/superpowers/plans/2026-06-27-canonical-toml-path-strings.md` -> `docs/historical/superpowers/plans/2026-06-27-canonical-toml-path-strings.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace structural path examples**
|
||||||
|
|
||||||
|
Use canonical examples:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"path":"input.text","target":"text"}
|
||||||
|
{"source":"result","target":"state.result"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Document TOML quoting with `state."field.with.dot"`. State that structural
|
||||||
|
objects are input-only compatibility and must not be generated by agents.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Record implementation status**
|
||||||
|
|
||||||
|
Mark the canonical-path section implemented in the design spec and add a short
|
||||||
|
completed roadmap entry.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run verification**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/core/test_path_values.py tests/core/test_canonical_node_bindings.py tests/authoring/test_path_inputs.py tests/authoring/test_builder.py tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_mcp/workflow_surface/test_drafts.py -q
|
||||||
|
uv run ruff check
|
||||||
|
uv run ruff format --check
|
||||||
|
uv run basedpyright --level error
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests pass, Ruff is clean, basedpyright reports zero errors, and
|
||||||
|
`git diff --check` reports no whitespace errors.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Archive and commit the plan**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git mv docs/superpowers/plans/2026-06-27-canonical-toml-path-strings.md docs/historical/superpowers/plans/2026-06-27-canonical-toml-path-strings.md
|
||||||
|
git add docs skills
|
||||||
|
git commit -m "docs: document canonical workflow path strings"
|
||||||
|
```
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
# Draft Semantic Authoring And Compile 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:** Separate semantic draft authoring from workspace lifecycle, add `branch` and `handle`, make capability-step routing complete, and expose read-only stored-workspace compilation.
|
||||||
|
|
||||||
|
**Architecture:** `WorkflowDraft` remains the only stored authoring document. A new `WorkflowDraftAuthoringApi` resolves capability metadata and lowers semantic intent to one atomic patch through `WorkflowDraftApi`; the latter retains lifecycle, validation, compilation, low-level edits, and JSON Patch. Public CLI, RPC, and MCP surfaces remain unified through `WorkflowApi`.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, Pydantic v2, JSON Patch, Typer, JSON-RPC, FastMCP, pytest, Ruff, basedpyright.
|
||||||
|
|
||||||
|
**Prerequisite:** Complete `docs/superpowers/plans/2026-06-27-canonical-toml-path-strings.md` first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Extract The Semantic Authoring Service Without Behavior Changes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_api/draft_authoring.py`
|
||||||
|
- Create: `src/wf_api/draft_payloads.py`
|
||||||
|
- Modify: `src/wf_api/drafts.py`
|
||||||
|
- Modify: `src/wf_api/service.py`
|
||||||
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add a facade delegation test**
|
||||||
|
|
||||||
|
Add a focused test asserting `WorkflowApi` constructs a sibling authoring
|
||||||
|
service and existing `bind_output_to_state` behavior still consumes one
|
||||||
|
revision.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Extract shared draft payload helpers**
|
||||||
|
|
||||||
|
Move the existing `_draft_step`, `_escape_json_pointer`,
|
||||||
|
`_draft_input_bindings_payload`, `_draft_output_bindings_payload`, and
|
||||||
|
`_state_root_field` bodies unchanged into `src/wf_api/draft_payloads.py`.
|
||||||
|
Rename them to `draft_step`, `escape_json_pointer`, `input_bindings_payload`,
|
||||||
|
`output_bindings_payload`, and `state_root_field`, update both service imports,
|
||||||
|
and add short docstrings. Do not change serialized behavior in this extraction.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Introduce `WorkflowDraftAuthoringApi`**
|
||||||
|
|
||||||
|
Use an explicit dependency on the lifecycle service:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WorkflowDraftAuthoringApi:
|
||||||
|
"""Capability-aware semantic edits over revisioned workflow drafts."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: WorkflowOperationContext,
|
||||||
|
drafts: WorkflowDraftApi,
|
||||||
|
) -> None:
|
||||||
|
self.context = context
|
||||||
|
self.drafts = drafts
|
||||||
|
```
|
||||||
|
|
||||||
|
Move `create_minimal_draft_workspace`, `bind_output_to_state`, and
|
||||||
|
`add_step_from_capability` into this class. Each operation must call
|
||||||
|
`self.drafts.patch_draft_workspace` or another public lifecycle method rather
|
||||||
|
than accessing the store directly for mutation.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Preserve the unified facade**
|
||||||
|
|
||||||
|
In `WorkflowApi.__init__`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.drafts = WorkflowDraftApi(context)
|
||||||
|
self.draft_authoring = WorkflowDraftAuthoringApi(context, self.drafts)
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing public delegates keep their names and forward semantic calls to
|
||||||
|
`self.draft_authoring`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run focused tests**
|
||||||
|
|
||||||
|
Run: `uv run pytest tests/wf_api/test_drafts_service.py -q`
|
||||||
|
|
||||||
|
Expected: PASS with no public behavior change.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/wf_api/draft_authoring.py src/wf_api/draft_payloads.py src/wf_api/drafts.py src/wf_api/service.py tests/wf_api/test_drafts_service.py
|
||||||
|
git commit -m "refactor: separate draft semantic authoring"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Remove The Superseded Partial State Projection Operation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/service.py`
|
||||||
|
- Modify: `src/wf_api/surface.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/models.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/methods/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/client/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/__init__.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/tools.py`
|
||||||
|
- Modify: `src/wf_cli/commands/drafts.py`
|
||||||
|
- Modify: affected draft tests in `tests/`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Prove no production caller remains**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
rg -n 'add_state_schema_from_output|add-state-from-output' src tests docs skills
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: only the operation implementation, adapters, tests, and docs refer to
|
||||||
|
it; no independent production caller depends on it.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove the operation end to end**
|
||||||
|
|
||||||
|
Delete `add_state_schema_from_output`, its request/params DTOs, RPC method,
|
||||||
|
client method, MCP tool, CLI command, exports, and dedicated tests. Do not add a
|
||||||
|
compatibility shim. Keep `bind_output_to_state` as the complete semantic
|
||||||
|
operation.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run surface import and help tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_mcp/server/test_config.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS and no removed command/tool in enumerated surfaces.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src tests
|
||||||
|
git commit -m "refactor: remove partial draft state projection"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Add Atomic `branch` And `handle` Authoring Operations
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/draft_authoring.py`
|
||||||
|
- Modify: `src/wf_api/service.py`
|
||||||
|
- Modify: `src/wf_api/surface.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/models.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/methods/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/client/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/__init__.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/tools.py`
|
||||||
|
- Modify: `src/wf_cli/commands/drafts.py`
|
||||||
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
|
- Test: `tests/wf_transport_rpc_http/test_app.py`
|
||||||
|
- Test: `tests/wf_transport_rpc_http/test_client.py`
|
||||||
|
- Test: `tests/wf_cli/test_remote_target.py`
|
||||||
|
- Test: `tests/wf_mcp/server/test_config.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing API tests**
|
||||||
|
|
||||||
|
Cover atomic route updates and preservation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await api.branch_draft(
|
||||||
|
workspace_id="branching",
|
||||||
|
revision=1,
|
||||||
|
step_id="classify",
|
||||||
|
routes={"ok": "next", "error": "tool_error"},
|
||||||
|
)
|
||||||
|
assert result["revision"] == 2
|
||||||
|
workspace = await api.get_draft_workspace(workspace_id="branching")
|
||||||
|
assert workspace["draft"]["routes"]["classify"] == {
|
||||||
|
"ok": "next",
|
||||||
|
"error": "tool_error",
|
||||||
|
"retry": "retry_step",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a `handle_draft` test updating two source outcomes in one revision. Assert
|
||||||
|
empty mappings, duplicate CLI values, unknown source steps, and unknown declared
|
||||||
|
outcomes leave the workspace byte-for-byte unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement semantic methods**
|
||||||
|
|
||||||
|
Use protocol-neutral signatures. `branch_draft` takes keyword-only
|
||||||
|
`workspace_id: str`, `revision: int`, `step_id: str`, and
|
||||||
|
`routes: dict[str, str]`. `handle_draft` takes keyword-only `workspace_id: str`,
|
||||||
|
`revision: int`, `branches: Sequence[DraftOutcomeRef]`, and `target: str`. Both
|
||||||
|
return `dict[str, Any]`.
|
||||||
|
|
||||||
|
Define `DraftOutcomeRef` as a small frozen Pydantic model or dataclass with
|
||||||
|
`step_id` and `outcome`. Validate request-local preconditions first, build one
|
||||||
|
JSON Patch list, then call `WorkflowDraftApi.patch_draft_workspace` once.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add transport models and methods**
|
||||||
|
|
||||||
|
RPC and MCP request models use structured branch records. Register:
|
||||||
|
|
||||||
|
```text
|
||||||
|
workflow.draft_workspaces.branch
|
||||||
|
workflow.draft_workspaces.handle
|
||||||
|
wf.workflow.branch_draft
|
||||||
|
wf.workflow.handle_draft
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `WorkflowDraftSurface` and the RPC client mixin with the exact API
|
||||||
|
signatures.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add CLI commands**
|
||||||
|
|
||||||
|
Expose:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wf draft branch WORKSPACE --revision N --step STEP --route ok=next --route error=fail
|
||||||
|
wf draft handle WORKSPACE --revision N --to fail --branch lookup:error --branch transform:error
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse route values with the existing strict `KEY=VALUE` utility. Parse branch
|
||||||
|
values at the final colon, reject duplicates, and send structured records.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run vertical-slice tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_remote_target.py tests/wf_mcp/server/test_config.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src tests
|
||||||
|
git commit -m "feat: add semantic draft branch and handle"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Make Capability-Step Routing Complete
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/draft_authoring.py`
|
||||||
|
- Modify: `src/wf_api/service.py`
|
||||||
|
- Modify: `src/wf_api/surface.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/models.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/methods/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/client/drafts.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/tools.py`
|
||||||
|
- Modify: `src/wf_cli/commands/drafts.py`
|
||||||
|
- Test: affected API/RPC/MCP/CLI draft tests
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing routing-policy tests**
|
||||||
|
|
||||||
|
Cover three cases:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# One declared outcome named "done", no routes supplied.
|
||||||
|
assert added_routes == {"done": "__end__"}
|
||||||
|
|
||||||
|
# No outcome metadata, no routes supplied.
|
||||||
|
assert added_routes == {"ok": "__end__"}
|
||||||
|
|
||||||
|
# Multiple declared outcomes, incomplete explicit routes.
|
||||||
|
with pytest.raises(ValueError, match="missing routes.*error"):
|
||||||
|
await api.add_step_from_capability(
|
||||||
|
workspace_id="multi",
|
||||||
|
revision=1,
|
||||||
|
step_id="echo",
|
||||||
|
capability_name="demo.echo",
|
||||||
|
routes={"ok": "__end__"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Also assert complete multi-outcome routes succeed in one revision.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace singular route parameters**
|
||||||
|
|
||||||
|
Replace `route_outcome` and `route_to` with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
routes: dict[str, str] | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolve declared capability outcomes before mutation. Infer the sole outcome
|
||||||
|
regardless of name; use `ok` only when metadata supplies none. For multiple
|
||||||
|
declared outcomes, require exact coverage and report `declared_outcomes`,
|
||||||
|
`missing_outcomes`, and `unknown_outcomes` in the application error.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update all public adapters**
|
||||||
|
|
||||||
|
RPC and MCP accept a route mapping. CLI replaces singular `--outcome`/`--to`
|
||||||
|
with repeatable `--route OUTCOME=TARGET`. Update help to explain sole-outcome
|
||||||
|
inference and multi-outcome completeness.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run focused tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/server/test_config.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src tests
|
||||||
|
git commit -m "fix: require complete capability step routes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Compile A Stored Draft Workspace Without Mutation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/drafts.py`
|
||||||
|
- Modify: `src/wf_api/service.py`
|
||||||
|
- Modify: `src/wf_api/surface.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/models.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/methods/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/client/drafts.py`
|
||||||
|
- Modify: `src/wf_transport_rpc_http/__init__.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||||
|
- Modify: `src/wf_mcp/workflow_surface/tools.py`
|
||||||
|
- Modify: `src/wf_cli/commands/drafts.py`
|
||||||
|
- Test: affected API/RPC/MCP/CLI draft tests
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing no-mutation API tests**
|
||||||
|
|
||||||
|
Capture the workspace before and after compilation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
before = await api.get_draft_workspace(workspace_id="compile_me")
|
||||||
|
result = await api.compile_draft_workspace(workspace_id="compile_me")
|
||||||
|
after = await api.get_draft_workspace(workspace_id="compile_me")
|
||||||
|
|
||||||
|
assert result["compiled_plan"]["name"] == "compile_me"
|
||||||
|
assert result["required_capabilities"]
|
||||||
|
assert after == before
|
||||||
|
```
|
||||||
|
|
||||||
|
Add an invalid-workspace test asserting structured diagnostics and no
|
||||||
|
`compiled_plan`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement the read-only projection**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def compile_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
|
||||||
|
workspace = self._draft_store().get_workspace(workspace_id)
|
||||||
|
validation = await self.validate_draft(draft=workspace.draft)
|
||||||
|
if validation["status"] != "valid":
|
||||||
|
return validation
|
||||||
|
return await self.compile_draft(draft=workspace.draft)
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not call `validate_draft_workspace`, because that operation refreshes stored
|
||||||
|
status and diagnostics.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Expose RPC and MCP operations**
|
||||||
|
|
||||||
|
Register:
|
||||||
|
|
||||||
|
```text
|
||||||
|
workflow.draft_workspaces.compile
|
||||||
|
wf.workflow.compile_draft_workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Return the application envelope containing `compiled_plan` and
|
||||||
|
`required_capabilities`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the CLI projection**
|
||||||
|
|
||||||
|
Expose `wf draft compile WORKSPACE`. On success print only
|
||||||
|
`result["compiled_plan"]`. On invalid status, print the structured diagnostic
|
||||||
|
envelope to stderr and exit nonzero. Do not add an output-file option.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run focused tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/workflow_surface/test_drafts.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src tests
|
||||||
|
git commit -m "feat: compile stored draft workspaces"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: Documentation, Skills, And End-To-End Regression
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/workflow_drafts.md`
|
||||||
|
- Modify: `docs/wf_cli.md`
|
||||||
|
- Modify: `docs/wf_authoring_control_flow.md`
|
||||||
|
- Modify: `skills/wf-cli/SKILL.md`
|
||||||
|
- Modify: `skills/wf-workflow/SKILL.md`
|
||||||
|
- Modify: `skills/wf-workflow/references/draft-workspaces.md`
|
||||||
|
- Modify: `skills/wf-workflow/references/workflow-lifecycle.md`
|
||||||
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
- Modify: `docs/superpowers/specs/2026-06-27-draft-semantic-authoring-boundary.md`
|
||||||
|
- Test: `tests/wf_cli/test_remote_target.py`
|
||||||
|
- Move after completion: `docs/superpowers/plans/2026-06-27-draft-semantic-authoring-and-compile.md` -> `docs/historical/superpowers/plans/2026-06-27-draft-semantic-authoring-and-compile.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add a two-step multi-outcome integration regression**
|
||||||
|
|
||||||
|
Use the running RPC test fixture or local static server to:
|
||||||
|
|
||||||
|
1. create a workspace from a capability;
|
||||||
|
2. add a second capability declaring `ok` and `error` with both routes;
|
||||||
|
3. validate without a follow-up `set-route` call;
|
||||||
|
4. save the artifact and deployment;
|
||||||
|
5. run it and assert two trace frames completed.
|
||||||
|
|
||||||
|
This test must fail against the old singular-route helper.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Reorganize public guidance by operation level**
|
||||||
|
|
||||||
|
Document semantic operations first, focused edits second, `patch` last. Explain
|
||||||
|
that `branch` and `handle` mirror `WorkflowBuilder` but mutate `WorkflowDraft`.
|
||||||
|
Document `compile` as read-only and show that it prints a raw plan without
|
||||||
|
saving an artifact.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Document extensibility without implementing extra step kinds**
|
||||||
|
|
||||||
|
State that `WorkflowDraftAuthoringApi` is the home for future semantic helpers
|
||||||
|
for `interrupt`, `foreach`, condition, join/end, and future core step kinds.
|
||||||
|
Do not add commands for them in this slice.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update status documents**
|
||||||
|
|
||||||
|
Mark the design implemented, record the completed roadmap item, update live
|
||||||
|
links, and archive this plan.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run final verification**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py -q
|
||||||
|
uv run ruff check
|
||||||
|
uv run ruff format --check
|
||||||
|
uv run basedpyright --level error
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests pass, Ruff is clean, basedpyright reports zero errors, and
|
||||||
|
there are no whitespace errors.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Archive and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git mv docs/superpowers/plans/2026-06-27-draft-semantic-authoring-and-compile.md docs/historical/superpowers/plans/2026-06-27-draft-semantic-authoring-and-compile.md
|
||||||
|
git add docs skills tests
|
||||||
|
git commit -m "docs: record semantic draft authoring surface"
|
||||||
|
```
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
# Draft Semantic Authoring Boundary
|
||||||
|
|
||||||
|
Date: 2026-06-27
|
||||||
|
|
||||||
|
Status: Approved for implementation planning.
|
||||||
|
|
||||||
|
Related:
|
||||||
|
|
||||||
|
- [Workflow drafts](../../workflow_drafts.md)
|
||||||
|
- [`wf_authoring` control flow](../../wf_authoring_control_flow.md)
|
||||||
|
- [Workflow API architecture](../../wf_api_architecture.md)
|
||||||
|
- [Current roadmap](../../current_roadmap.md)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define a stable boundary between the persisted draft document, revisioned draft
|
||||||
|
workspaces, and capability-aware authoring operations intended for agents.
|
||||||
|
|
||||||
|
The draft system must remain one authoring model rather than becoming a second
|
||||||
|
workflow language beside `WorkflowBuilder`. Semantic operations therefore use
|
||||||
|
the same control-flow vocabulary as `WorkflowBuilder`, but lower each intent to
|
||||||
|
one atomic patch against the persisted `WorkflowDraft` representation.
|
||||||
|
|
||||||
|
## Current Problem
|
||||||
|
|
||||||
|
The current `WorkflowDraftApi` owns several distinct responsibilities:
|
||||||
|
|
||||||
|
- draft parsing, validation, and compilation;
|
||||||
|
- revisioned workspace lifecycle and JSON Patch application;
|
||||||
|
- low-level focused document edits;
|
||||||
|
- capability lookup and schema projection;
|
||||||
|
- compound authoring operations such as adding a capability step.
|
||||||
|
|
||||||
|
The responsibilities are individually valid, but keeping them in one service
|
||||||
|
obscures the boundary and allows semantic behavior to diverge from
|
||||||
|
`WorkflowBuilder`. The existing `add_step_from_capability` demonstrates this:
|
||||||
|
it writes one outgoing route even when the capability declares multiple
|
||||||
|
outcomes, producing a draft that immediately fails validation.
|
||||||
|
|
||||||
|
## Core Decisions
|
||||||
|
|
||||||
|
### One Persisted Authoring Representation
|
||||||
|
|
||||||
|
`WorkflowDraft` remains the only persisted authoring representation. It is a
|
||||||
|
patch-friendly intermediate form with keyed `steps`, `routes`, schemas, and
|
||||||
|
explicit bindings. `RawWorkflowPlan` remains the normalized execution model.
|
||||||
|
|
||||||
|
```text
|
||||||
|
semantic authoring intent
|
||||||
|
-> atomic WorkflowDraft patch
|
||||||
|
-> revision check
|
||||||
|
-> compile and validate
|
||||||
|
-> persisted WorkflowDraft workspace
|
||||||
|
-> RawWorkflowPlan projection
|
||||||
|
```
|
||||||
|
|
||||||
|
Semantic operations do not persist builder objects, create an additional graph
|
||||||
|
model, or modify raw plans in place.
|
||||||
|
|
||||||
|
### Separate Semantic Authoring Service
|
||||||
|
|
||||||
|
Introduce `WorkflowDraftAuthoringApi` as a sibling of `WorkflowDraftApi`.
|
||||||
|
|
||||||
|
`WorkflowDraftApi` owns:
|
||||||
|
|
||||||
|
- workspace create, get, list, delete, and revision handling;
|
||||||
|
- draft validation and compilation;
|
||||||
|
- raw JSON Patch;
|
||||||
|
- low-level focused document edits;
|
||||||
|
- read-only projection of a stored workspace to `RawWorkflowPlan`.
|
||||||
|
|
||||||
|
`WorkflowDraftAuthoringApi` owns:
|
||||||
|
|
||||||
|
- capability-aware draft bootstrap;
|
||||||
|
- semantic construction of draft step kinds;
|
||||||
|
- adding a capability-backed `use` step;
|
||||||
|
- projecting and binding capability outputs into state;
|
||||||
|
- `branch` and `handle` semantic routing operations.
|
||||||
|
|
||||||
|
`WorkflowDraftAuthoringApi` depends on `WorkflowDraftApi` for workspace access
|
||||||
|
and patch application. It must not write the workspace store directly. Every
|
||||||
|
semantic operation produces one patch and consumes one revision.
|
||||||
|
|
||||||
|
`WorkflowApi` continues to expose one protocol-neutral facade. RPC, MCP, and CLI
|
||||||
|
clients do not need to know about the internal service split.
|
||||||
|
|
||||||
|
The service boundary is intentionally not capability-only. The current draft
|
||||||
|
model also represents `end`, `condition`, `interrupt`, `foreach`, `join`,
|
||||||
|
`when`, `choose`, and `match` steps, and core may gain more step kinds. This
|
||||||
|
slice adds semantic operations only where required, but new step-kind helpers
|
||||||
|
belong in `WorkflowDraftAuthoringApi` rather than a parallel authoring system.
|
||||||
|
|
||||||
|
### Match `WorkflowBuilder` Vocabulary
|
||||||
|
|
||||||
|
The semantic draft surface uses the established authoring terms:
|
||||||
|
|
||||||
|
- `branch`: connect several outcomes from one existing step to targets;
|
||||||
|
- `handle`: connect several source-step/outcome pairs to one shared target.
|
||||||
|
|
||||||
|
These operations add or replace edges only. They do not create condition steps,
|
||||||
|
wait for concurrent branches, or implement join semantics. `match`, `when`, and
|
||||||
|
`choose` remain outside this first slice.
|
||||||
|
|
||||||
|
## Public Operation Levels
|
||||||
|
|
||||||
|
The public draft surface is documented in descending order of preference.
|
||||||
|
|
||||||
|
### Semantic Authoring Operations
|
||||||
|
|
||||||
|
- `create-from-capability`
|
||||||
|
- `add-step-from-capability`
|
||||||
|
- `bind-output-to-state`
|
||||||
|
- `branch`
|
||||||
|
- `handle`
|
||||||
|
|
||||||
|
These operations understand capability definitions, schemas, outcomes, or
|
||||||
|
graph intent. They are the preferred agent authoring surface.
|
||||||
|
|
||||||
|
### Low-Level Focused Edits
|
||||||
|
|
||||||
|
- `set-name`
|
||||||
|
- `set-route`
|
||||||
|
- `set-input`
|
||||||
|
- `set-output`
|
||||||
|
|
||||||
|
These remain available for precise repairs. `set-output` does not project the
|
||||||
|
destination state schema; callers should prefer `bind-output-to-state` when
|
||||||
|
writing a capability output into state.
|
||||||
|
|
||||||
|
### Escape Hatch
|
||||||
|
|
||||||
|
`patch` remains the RFC 6902 escape hatch for structural edits that semantic or
|
||||||
|
focused operations do not cover.
|
||||||
|
|
||||||
|
### Lifecycle And Projection
|
||||||
|
|
||||||
|
- `list`
|
||||||
|
- `inspect`
|
||||||
|
- `validate`
|
||||||
|
- `compile`
|
||||||
|
- `save`
|
||||||
|
- `delete`
|
||||||
|
|
||||||
|
These operations manage or inspect the workspace rather than expressing graph
|
||||||
|
authoring intent.
|
||||||
|
|
||||||
|
## Operation Contracts
|
||||||
|
|
||||||
|
### Add Step From Capability
|
||||||
|
|
||||||
|
`add-step-from-capability` atomically adds:
|
||||||
|
|
||||||
|
- one capability-backed `use` step;
|
||||||
|
- explicit input bindings;
|
||||||
|
- output-to-state bindings and required state schema projection;
|
||||||
|
- an optional incoming route;
|
||||||
|
- the complete outgoing route map.
|
||||||
|
|
||||||
|
The outgoing CLI option is repeatable:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wf draft add-step-from-capability WORKSPACE `
|
||||||
|
--revision 4 `
|
||||||
|
--step second_echo `
|
||||||
|
--capability everything.default.echo `
|
||||||
|
--route ok=next `
|
||||||
|
--route error=tool_error
|
||||||
|
```
|
||||||
|
|
||||||
|
When the caller supplies no routes and the capability declares exactly one
|
||||||
|
outcome, the operation infers that outcome, regardless of its name, and routes
|
||||||
|
it to `__end__`. If capability metadata declares no outcomes, the inferred
|
||||||
|
outcome is `ok`. When a capability has multiple known outcomes, the caller must
|
||||||
|
provide a target for every outcome. Missing or unknown outcomes reject the
|
||||||
|
operation before mutation and report the declared outcomes. Callers can always
|
||||||
|
override the target by supplying an explicit route.
|
||||||
|
|
||||||
|
The current singular `route_outcome` and `route_to` shape has no known external
|
||||||
|
caller or persisted-data dependency and is replaced rather than retained as
|
||||||
|
ghost compatibility behavior.
|
||||||
|
|
||||||
|
### Branch
|
||||||
|
|
||||||
|
`branch` applies several outcome routes from one existing step in one revision:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wf draft branch WORKSPACE --revision 5 --step classify `
|
||||||
|
--route send=send_email `
|
||||||
|
--route skip=__end__ `
|
||||||
|
--route error=tool_error
|
||||||
|
```
|
||||||
|
|
||||||
|
Supplied outcomes add or replace their route. Routes for outcomes omitted from
|
||||||
|
the request remain unchanged. The operation rejects an empty route map, unknown
|
||||||
|
source step, unknown declared outcome, or malformed target before mutation.
|
||||||
|
Normal workflow validation remains responsible for missing required outcomes,
|
||||||
|
unknown target steps, and broader graph consistency.
|
||||||
|
|
||||||
|
### Handle
|
||||||
|
|
||||||
|
`handle` redirects several source-step/outcome pairs to one shared target:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wf draft handle WORKSPACE --revision 6 --to tool_error `
|
||||||
|
--branch lookup:error `
|
||||||
|
--branch transform:error
|
||||||
|
```
|
||||||
|
|
||||||
|
The transport request uses structured pairs rather than encoded strings. The
|
||||||
|
CLI parses each `STEP:OUTCOME` value at the final colon and rejects malformed
|
||||||
|
values before making the request. Existing routes unrelated to the supplied
|
||||||
|
pairs remain unchanged.
|
||||||
|
|
||||||
|
`handle` is not a join. It creates ordinary directed edges to one target.
|
||||||
|
|
||||||
|
### Bind Output To State
|
||||||
|
|
||||||
|
`bind-output-to-state` remains the capability-aware schema propagation
|
||||||
|
operation. It projects the selected output property and required `$defs` into
|
||||||
|
the root state schema, then merges the output binding in the same revision.
|
||||||
|
|
||||||
|
The partial `add-state-from-output` operation is removed from API, RPC, MCP,
|
||||||
|
CLI, docs, and skills. It was superseded before acquiring a real caller or
|
||||||
|
persisted-data contract.
|
||||||
|
|
||||||
|
### Compile A Stored Workspace
|
||||||
|
|
||||||
|
Add a read-only workspace projection:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wf draft compile WORKSPACE
|
||||||
|
```
|
||||||
|
|
||||||
|
The server operation:
|
||||||
|
|
||||||
|
1. reads the stored workspace;
|
||||||
|
2. validates it in memory against current capability definitions;
|
||||||
|
3. compiles it through the existing draft adapter;
|
||||||
|
4. returns `compiled_plan` and required capability metadata.
|
||||||
|
|
||||||
|
The CLI prints only the bare `compiled_plan` JSON so it can be inspected or
|
||||||
|
piped directly into another command. The operation does not save an artifact,
|
||||||
|
refresh stored diagnostics, increment the revision, or otherwise mutate the
|
||||||
|
workspace.
|
||||||
|
|
||||||
|
An invalid workspace returns structured diagnostics and a nonzero CLI exit. It
|
||||||
|
must not emit a partial raw plan.
|
||||||
|
|
||||||
|
## Validation And Error Behavior
|
||||||
|
|
||||||
|
All semantic mutations use the current workspace revision. A stale revision,
|
||||||
|
malformed request, unknown capability, or semantic precondition failure leaves
|
||||||
|
the workspace unchanged.
|
||||||
|
|
||||||
|
Once a semantic patch is constructed, it passes through the existing workspace
|
||||||
|
patch path. That path performs the revision check, draft parsing, compilation,
|
||||||
|
structural validation, persistence, and refreshed diagnostics.
|
||||||
|
|
||||||
|
Known request-local mistakes should fail before mutation with specific data:
|
||||||
|
|
||||||
|
- multi-outcome step missing routes: include missing and declared outcomes;
|
||||||
|
- branch with unknown outcome: include the step's declared outcomes;
|
||||||
|
- handle with unknown source step: identify the missing step;
|
||||||
|
- duplicate route or branch values in one CLI invocation: reject as ambiguous;
|
||||||
|
- invalid compile: return the same structured diagnostic vocabulary as draft
|
||||||
|
validation.
|
||||||
|
|
||||||
|
Draft workspaces may remain invalid during iterative low-level editing. Semantic
|
||||||
|
operations should avoid creating a known-invalid result when all required
|
||||||
|
information is already available in the request and capability catalog.
|
||||||
|
|
||||||
|
## Transport Shape
|
||||||
|
|
||||||
|
The protocol-neutral API uses mappings and structured records:
|
||||||
|
|
||||||
|
- branch routes: `dict[str, str]` mapping outcome to target;
|
||||||
|
- handle branches: a list of `{step_id, outcome}` records plus one target;
|
||||||
|
- add-step routes: `dict[str, str]` mapping every declared outcome to target.
|
||||||
|
|
||||||
|
RPC request models and MCP request models mirror those shapes. CLI parsing is a
|
||||||
|
front-end concern and must not leak encoded `STEP:OUTCOME` strings into the
|
||||||
|
application API.
|
||||||
|
|
||||||
|
## Canonical Path Strings
|
||||||
|
|
||||||
|
Authoring surfaces use one canonical TOML-key path grammar. Examples include:
|
||||||
|
|
||||||
|
```text
|
||||||
|
state.report.title
|
||||||
|
input."customer.name"
|
||||||
|
local.items
|
||||||
|
```
|
||||||
|
|
||||||
|
The underlying `GraphSourcePath`, `StatePath`, and `LocalPath` models remain
|
||||||
|
structured typed values. Strings are the public and serialized representation;
|
||||||
|
the models parse those strings once at their boundary.
|
||||||
|
|
||||||
|
Move TOML-key parsing from the `wf_authoring` convenience layer into `wf_core`
|
||||||
|
so CLI, RPC, MCP, drafts, raw plans, and Python authoring use the same parser and
|
||||||
|
formatter. The shared grammar must support quoted TOML keys for literal dots,
|
||||||
|
spaces, and other non-bare segments. Parse errors identify the complete input
|
||||||
|
and recommend quoting the invalid segment.
|
||||||
|
|
||||||
|
Pydantic JSON schemas advertise path strings rather than the structural
|
||||||
|
`{root, parts}` object. Serializers emit canonical strings. Validators continue
|
||||||
|
to accept the structural object only as a read-compatibility path for existing
|
||||||
|
persisted drafts, artifacts, and runs; new public examples and writes use
|
||||||
|
strings. This is compatibility for real stored data, not a second documented
|
||||||
|
syntax.
|
||||||
|
|
||||||
|
## Compatibility And Migration
|
||||||
|
|
||||||
|
No workflow semantics or draft field layout changes. The serialized path
|
||||||
|
representation changes from structural objects to canonical strings. Existing
|
||||||
|
workspaces, artifacts, deployments, and runs remain readable: their path
|
||||||
|
objects are accepted on input and become canonical strings when a containing
|
||||||
|
record is rewritten.
|
||||||
|
|
||||||
|
Low-level operations remain available. The migration changes only compound
|
||||||
|
operation signatures and removes the unused partial schema helper. Repository
|
||||||
|
callers, tests, docs, and skills are updated in the same slice. No compatibility
|
||||||
|
shim is added without a real external caller.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Domain And Service Tests
|
||||||
|
|
||||||
|
- branch merges supplied routes and preserves unrelated routes;
|
||||||
|
- handle updates several source routes atomically;
|
||||||
|
- malformed requests and stale revisions do not mutate the workspace;
|
||||||
|
- multi-outcome add-step requires complete routes;
|
||||||
|
- route inference chooses the sole declared outcome even when it is not `ok`;
|
||||||
|
- absent outcome metadata falls back to `ok`;
|
||||||
|
- an inferred route targets `__end__` unless explicitly overridden;
|
||||||
|
- output binding still projects referenced schema definitions;
|
||||||
|
- all path models parse and serialize the canonical TOML-key string grammar;
|
||||||
|
- structural path objects remain readable but are not emitted;
|
||||||
|
- stored-workspace compile equals `compile_workflow_draft` output;
|
||||||
|
- compile does not change revision, timestamps, status, or diagnostics.
|
||||||
|
|
||||||
|
### Surface Tests
|
||||||
|
|
||||||
|
- RPC and client methods preserve structured route data;
|
||||||
|
- MCP tools expose branch, handle, and workspace compile;
|
||||||
|
- CLI repeatable options parse into the protocol-neutral request shape;
|
||||||
|
- CLI compile prints only the raw plan and exits nonzero for invalid drafts;
|
||||||
|
- help text distinguishes semantic operations, low-level edits, and JSON Patch.
|
||||||
|
|
||||||
|
### Integration Regression
|
||||||
|
|
||||||
|
Build a two-step workflow where the second capability declares `ok` and
|
||||||
|
`error`. Add it with complete routes, save the artifact and deployment, run it,
|
||||||
|
and verify both steps execute without requiring a follow-up `set-route` repair.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- adding `match`, `when`, or `choose` draft commands in this slice;
|
||||||
|
- changing workflow semantics or the `WorkflowDraft` field layout;
|
||||||
|
- replacing JSON Patch;
|
||||||
|
- treating `handle` as synchronization or join behavior;
|
||||||
|
- automatic semantic compatibility analysis between connected schemas;
|
||||||
|
- saving artifacts as a side effect of compile.
|
||||||
Reference in New Issue
Block a user