docs: complete generic draft step authoring
This commit is contained in:
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
## Draft authoring parity
|
## Draft authoring parity
|
||||||
|
|
||||||
- [ ] No dedicated draft CLI subcommands for adding non-capability step kinds.
|
- [x] Dedicated draft CLI subcommands cover every draft step kind.
|
||||||
- [ ] `DraftInterruptPayload` cannot preserve `request_schema` or
|
- [x] Draft interrupts preserve request and resume schemas.
|
||||||
`resume_schema`, so typed interrupt contracts cannot be authored through the
|
- [x] Draft subgraphs preserve workflow references and boundary contracts.
|
||||||
draft model.
|
|
||||||
- [ ] `DraftStep` and its adapter have no subgraph representation even though
|
|
||||||
`SubgraphNode` is a canonical core step.
|
|
||||||
|
|||||||
@@ -550,12 +550,12 @@ clear operator feedback before adding more architecture.
|
|||||||
output-to-state helper and reducing manual draft patch repairs in agent
|
output-to-state helper and reducing manual draft patch repairs in agent
|
||||||
challenge runs.
|
challenge runs.
|
||||||
- Completed: draft CLI vocabulary now uses `wf draft create --capability` and
|
- Completed: draft CLI vocabulary now uses `wf draft create --capability` and
|
||||||
`wf draft add-step --capability`, replacing the longer
|
the typed `wf draft add <kind>` command tree. All nine draft step kinds have
|
||||||
`*-from-capability` commands that agents repeatedly guessed around.
|
dedicated commands; capability insertion retains schema projection, while
|
||||||
- Completed: `wf draft add-step` inserts one explicit
|
interrupt and subgraph commands preserve their explicit boundary contracts.
|
||||||
capability-backed step with route, input, and output-to-state schema/binding
|
Generic insertion is exposed through Python and JSON-RPC with atomic incoming
|
||||||
wiring in a single revision, reducing brittle JSON Patch authoring for
|
and outgoing route wiring. Implementation:
|
||||||
multi-step workflows. Accepts `--route OUTCOME=TARGET` for multi-outcome steps.
|
[`generic draft step authoring`](historical/superpowers/plans/2026-07-20-generic-draft-add-step.md).
|
||||||
- Completed: `wf draft branch` and `wf draft handle` provide atomic route
|
- Completed: `wf draft branch` and `wf draft handle` provide atomic route
|
||||||
editing for existing draft steps without rewriting the full routes object.
|
editing for existing draft steps without rewriting the full routes object.
|
||||||
- Completed: `wf draft compile` returns the compiled raw plan plus required
|
- Completed: `wf draft compile` returns the compiled raw plan plus required
|
||||||
@@ -786,7 +786,7 @@ stable.
|
|||||||
[`required-only wrapper inputs`](historical/superpowers/plans/2026-06-29-required-only-wrapper-inputs.md).
|
[`required-only wrapper inputs`](historical/superpowers/plans/2026-06-29-required-only-wrapper-inputs.md).
|
||||||
- Completed: `wf draft set-input` rejects `local.x` targets before RPC and
|
- Completed: `wf draft set-input` rejects `local.x` targets before RPC and
|
||||||
shows the equivalent bare-target mapping.
|
shows the equivalent bare-target mapping.
|
||||||
- Completed: `wf draft add-step --route` errors include declared outcomes and
|
- Completed: `wf draft add capability --route` errors include declared outcomes and
|
||||||
direct add/remove repair guidance.
|
direct add/remove repair guidance.
|
||||||
- Completed: repeated idempotent `wf draft bind input/state -> local` behavior
|
- Completed: repeated idempotent `wf draft bind input/state -> local` behavior
|
||||||
is covered by regression tests.
|
is covered by regression tests.
|
||||||
|
|||||||
+48
-48
@@ -54,7 +54,7 @@
|
|||||||
- Produces: `DraftSubgraphPayload`, `DraftSubgraphStep`, expanded `DraftInterruptPayload`, and updated `DraftStep`.
|
- Produces: `DraftSubgraphPayload`, `DraftSubgraphStep`, expanded `DraftInterruptPayload`, and updated `DraftStep`.
|
||||||
- Produces: adapter lowering to `InterruptNode` and `SubgraphNode` with contracts intact.
|
- Produces: adapter lowering to `InterruptNode` and `SubgraphNode` with contracts intact.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing model tests for typed interrupt contracts**
|
- [x] **Step 1: Write failing model tests for typed interrupt contracts**
|
||||||
|
|
||||||
Add a draft containing:
|
Add a draft containing:
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ Add a draft containing:
|
|||||||
|
|
||||||
Assert the parsed fields and `model_dump(mode="json", by_alias=True)` preserve both schemas.
|
Assert the parsed fields and `model_dump(mode="json", by_alias=True)` preserve both schemas.
|
||||||
|
|
||||||
- [ ] **Step 2: Write failing model tests for subgraph boundaries**
|
- [x] **Step 2: Write failing model tests for subgraph boundaries**
|
||||||
|
|
||||||
Cover both workflow reference forms:
|
Cover both workflow reference forms:
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ Cover both workflow reference forms:
|
|||||||
|
|
||||||
Assert `DraftSubgraphStep` is selected and aliases round-trip.
|
Assert `DraftSubgraphStep` is selected and aliases round-trip.
|
||||||
|
|
||||||
- [ ] **Step 3: Run model tests and confirm red**
|
- [x] **Step 3: Run model tests and confirm red**
|
||||||
|
|
||||||
Run: `uv run pytest tests/artifacts/test_draft_models.py -q`
|
Run: `uv run pytest tests/artifacts/test_draft_models.py -q`
|
||||||
|
|
||||||
Expected: failures for forbidden interrupt schema fields and unknown `subgraph` kind.
|
Expected: failures for forbidden interrupt schema fields and unknown `subgraph` kind.
|
||||||
|
|
||||||
- [ ] **Step 4: Implement the draft model fields and union member**
|
- [x] **Step 4: Implement the draft model fields and union member**
|
||||||
|
|
||||||
Import `SchemaRef` and `WorkflowRef`, add `subgraph` to `STEP_KIND_KEYS`, add the payload/step classes from the approved design, and append `DraftSubgraphStep` to `DraftStep`. Add to `DraftInterruptPayload`:
|
Import `SchemaRef` and `WorkflowRef`, add `subgraph` to `STEP_KIND_KEYS`, add the payload/step classes from the approved design, and append `DraftSubgraphStep` to `DraftStep`. Add to `DraftInterruptPayload`:
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ Add a field validator that accepts `None` and rejects a supplied schema unless
|
|||||||
`schema.type == "object"`. This keeps untyped interrupts untyped while
|
`schema.type == "object"`. This keeps untyped interrupts untyped while
|
||||||
validating explicit contracts at draft parse time.
|
validating explicit contracts at draft parse time.
|
||||||
|
|
||||||
- [ ] **Step 5: Write failing adapter tests**
|
- [x] **Step 5: Write failing adapter tests**
|
||||||
|
|
||||||
Assert `build_workflow_from_draft` produces:
|
Assert `build_workflow_from_draft` produces:
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ assert child.output_schema == output_schema
|
|||||||
assert child.outcomes == ["ok", "error"]
|
assert child.outcomes == ["ok", "error"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 6: Implement adapter lowering**
|
- [x] **Step 6: Implement adapter lowering**
|
||||||
|
|
||||||
Build interrupt keyword arguments so `request_schema`/`resume_schema` are
|
Build interrupt keyword arguments so `request_schema`/`resume_schema` are
|
||||||
omitted when `None`; passing object defaults would incorrectly set
|
omitted when `None`; passing object defaults would incorrectly set
|
||||||
@@ -149,7 +149,7 @@ return node
|
|||||||
|
|
||||||
Use an explicit `isinstance(step, DraftSubgraphStep)` branch before the final `TypeError`.
|
Use an explicit `isinstance(step, DraftSubgraphStep)` branch before the final `TypeError`.
|
||||||
|
|
||||||
- [ ] **Step 7: Verify and commit**
|
- [x] **Step 7: Verify and commit**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ git commit -m "feat: complete draft step model parity"
|
|||||||
- Consumes: `DraftStep` including `DraftSubgraphStep` from Task 1.
|
- Consumes: `DraftStep` including `DraftSubgraphStep` from Task 1.
|
||||||
- Produces: `RouteSource` and `WorkflowApiSurface.add_step(*, workspace_id, revision, step_id, step, incoming, routes)`.
|
- Produces: `RouteSource` and `WorkflowApiSurface.add_step(*, workspace_id, revision, step_id, step, incoming, routes)`.
|
||||||
|
|
||||||
- [ ] **Step 1: Rename the internal route value object**
|
- [x] **Step 1: Rename the internal route value object**
|
||||||
|
|
||||||
Replace `DraftOutcomeRef` with:
|
Replace `DraftOutcomeRef` with:
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ class RouteSource:
|
|||||||
|
|
||||||
Update `handle_draft`, `WorkflowApi.handle_draft`, imports, and existing tests. Do not retain an alias because all callers are repository-owned.
|
Update `handle_draft`, `WorkflowApi.handle_draft`, imports, and existing tests. Do not retain an alias because all callers are repository-owned.
|
||||||
|
|
||||||
- [ ] **Step 2: Write failing parameterized insertion tests**
|
- [x] **Step 2: Write failing parameterized insertion tests**
|
||||||
|
|
||||||
Parameterize the nine payloads (`use`, `foreach`, `interrupt`, `join`, `end`, `when`, `choose`, `match`, `subgraph`). For each, call:
|
Parameterize the nine payloads (`use`, `foreach`, `interrupt`, `join`, `end`, `when`, `choose`, `match`, `subgraph`). For each, call:
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ result = await api.add_step(
|
|||||||
|
|
||||||
Use a Pydantic `TypeAdapter(DraftStep)` in the test and assert revision `2` plus the canonical dumped payload under `draft.steps.new_step`.
|
Use a Pydantic `TypeAdapter(DraftStep)` in the test and assert revision `2` plus the canonical dumped payload under `draft.steps.new_step`.
|
||||||
|
|
||||||
- [ ] **Step 3: Write failing atomic routing/error tests**
|
- [x] **Step 3: Write failing atomic routing/error tests**
|
||||||
|
|
||||||
Cover:
|
Cover:
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ Cover:
|
|||||||
- incomplete but valid route subsets accepted;
|
- incomplete but valid route subsets accepted;
|
||||||
- each failure leaves revision and draft bytes unchanged.
|
- each failure leaves revision and draft bytes unchanged.
|
||||||
|
|
||||||
- [ ] **Step 4: Implement declared-outcome validation**
|
- [x] **Step 4: Implement declared-outcome validation**
|
||||||
|
|
||||||
Add a private helper with exhaustive `isinstance` branches:
|
Add a private helper with exhaustive `isinstance` branches:
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ def _draft_step_route_outcomes(self, step: DraftStep) -> set[str] | None:
|
|||||||
|
|
||||||
`None` means top-level routes are forbidden, not unknown.
|
`None` means top-level routes are forbidden, not unknown.
|
||||||
|
|
||||||
- [ ] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`**
|
- [x] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`**
|
||||||
|
|
||||||
Build a patch only after all checks pass:
|
Build a patch only after all checks pass:
|
||||||
|
|
||||||
@@ -294,11 +294,11 @@ return await self.drafts.patch_draft_workspace(
|
|||||||
|
|
||||||
Check `steps`, `routes`, duplicate id, incoming source existence, forbidden routes, and unknown route keys before this call.
|
Check `steps`, `routes`, duplicate id, incoming source existence, forbidden routes, and unknown route keys before this call.
|
||||||
|
|
||||||
- [ ] **Step 6: Expose the method through service and surface**
|
- [x] **Step 6: Expose the method through service and surface**
|
||||||
|
|
||||||
Use the exact signature from the design in both `WorkflowApi` and `WorkflowApiSurface`. The service method delegates to `self.draft_authoring.add_step` without converting the typed step back to a raw dict.
|
Use the exact signature from the design in both `WorkflowApi` and `WorkflowApiSurface`. The service method delegates to `self.draft_authoring.add_step` without converting the typed step back to a raw dict.
|
||||||
|
|
||||||
- [ ] **Step 7: Verify and commit**
|
- [x] **Step 7: Verify and commit**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ git commit -m "feat: add atomic generic draft step insertion"
|
|||||||
- Consumes: `WorkflowApiSurface.add_step`, `DraftStep`, and `RouteSource` from Task 2.
|
- Consumes: `WorkflowApiSurface.add_step`, `DraftStep`, and `RouteSource` from Task 2.
|
||||||
- Produces: method `workflow.draft_workspaces.add_step` and remote client parity.
|
- Produces: method `workflow.draft_workspaces.add_step` and remote client parity.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing RPC parameter tests**
|
- [x] **Step 1: Write failing RPC parameter tests**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ class AddDraftStepParams(RpcParamsModel):
|
|||||||
|
|
||||||
Before implementation, tests should attempt to import the models and validate a foreach alias (`as`), a when alias (`if`), typed interrupt schemas, and a subgraph artifact reference. Add malformed tests for unknown/multiple kind keys and blank route-source fields.
|
Before implementation, tests should attempt to import the models and validate a foreach alias (`as`), a when alias (`if`), typed interrupt schemas, and a subgraph artifact reference. Add malformed tests for unknown/multiple kind keys and blank route-source fields.
|
||||||
|
|
||||||
- [ ] **Step 2: Implement parameter models and canonical serialization tests**
|
- [x] **Step 2: Implement parameter models and canonical serialization tests**
|
||||||
|
|
||||||
Import `DraftStep` from `wf_artifacts.drafts`. Assert:
|
Import `DraftStep` from `wf_artifacts.drafts`. Assert:
|
||||||
|
|
||||||
@@ -360,11 +360,11 @@ assert dumped["step"]["foreach"]["as"] == "item"
|
|||||||
assert "as_" not in dumped["step"]["foreach"]
|
assert "as_" not in dumped["step"]["foreach"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Write a failing server round-trip test**
|
- [x] **Step 3: Write a failing server round-trip test**
|
||||||
|
|
||||||
Call `workflow.draft_workspaces.add_step` against a temporary store with a typed interrupt step, incoming source, and routes. Assert one revision increment and preserved request/resume schemas. Add a malformed RPC request and assert the workspace is unchanged.
|
Call `workflow.draft_workspaces.add_step` against a temporary store with a typed interrupt step, incoming source, and routes. Assert one revision increment and preserved request/resume schemas. Add a malformed RPC request and assert the workspace is unchanged.
|
||||||
|
|
||||||
- [ ] **Step 4: Register the method**
|
- [x] **Step 4: Register the method**
|
||||||
|
|
||||||
Add to `methods/drafts.py`:
|
Add to `methods/drafts.py`:
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ async def workflow_draft_workspaces_add_step(
|
|||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Write failing client request-shape tests**
|
- [x] **Step 5: Write failing client request-shape tests**
|
||||||
|
|
||||||
Use the existing recording transport fixture. Assert exact method name and payload:
|
Use the existing recording transport fixture. Assert exact method name and payload:
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ assert request["params"]["incoming"] == {"step_id": "lookup", "outcome": "ok"}
|
|||||||
|
|
||||||
Parameterize all nine variants so alias/schema/reference fields cannot be dropped.
|
Parameterize all nine variants so alias/schema/reference fields cannot be dropped.
|
||||||
|
|
||||||
- [ ] **Step 6: Implement the client method**
|
- [x] **Step 6: Implement the client method**
|
||||||
|
|
||||||
The client accepts typed values and dumps aliases explicitly:
|
The client accepts typed values and dumps aliases explicitly:
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ async def add_step(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 7: Verify and commit**
|
- [x] **Step 7: Verify and commit**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ git commit -m "feat: expose generic draft steps over rpc"
|
|||||||
- Consumes: `WorkflowApiSurface.add_step` and existing `add_step_from_capability`.
|
- Consumes: `WorkflowApiSurface.add_step` and existing `add_step_from_capability`.
|
||||||
- Produces: `draft_add.app` registered as `wf draft add` and migrated `capability` command.
|
- Produces: `draft_add.app` registered as `wf draft add` and migrated `capability` command.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing command-tree tests**
|
- [x] **Step 1: Write failing command-tree tests**
|
||||||
|
|
||||||
Assert:
|
Assert:
|
||||||
|
|
||||||
@@ -486,7 +486,7 @@ removed = runner.invoke(app, ["draft", "add-step", "--help"])
|
|||||||
assert removed.exit_code != 0
|
assert removed.exit_code != 0
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Extract only shared parser helpers**
|
- [x] **Step 2: Extract only shared parser helpers**
|
||||||
|
|
||||||
Move `_parse_assignment_flags`, `_parse_map_flags`, `_parse_output_map_flags`, `_parse_step_input_map_flags`, and `_parse_route_flags` from `drafts.py` into `draft_options.py`. Add:
|
Move `_parse_assignment_flags`, `_parse_map_flags`, `_parse_output_map_flags`, `_parse_step_input_map_flags`, and `_parse_route_flags` from `drafts.py` into `draft_options.py`. Add:
|
||||||
|
|
||||||
@@ -511,7 +511,7 @@ def route_source(from_step: str | None, from_outcome: str | None) -> RouteSource
|
|||||||
|
|
||||||
Keep imports updated so existing draft commands retain identical parsing.
|
Keep imports updated so existing draft commands retain identical parsing.
|
||||||
|
|
||||||
- [ ] **Step 3: Create and register the subgroup**
|
- [x] **Step 3: Create and register the subgroup**
|
||||||
|
|
||||||
In `draft_add.py`:
|
In `draft_add.py`:
|
||||||
|
|
||||||
@@ -525,7 +525,7 @@ app = typer.Typer(
|
|||||||
|
|
||||||
In `drafts.py`, import `draft_add` and register `app.add_typer(draft_add.app, name="add")` after constructing the draft app.
|
In `drafts.py`, import `draft_add` and register `app.add_typer(draft_add.app, name="add")` after constructing the draft app.
|
||||||
|
|
||||||
- [ ] **Step 4: Move the capability command without changing behavior**
|
- [x] **Step 4: Move the capability command without changing behavior**
|
||||||
|
|
||||||
Register the existing body as `@app.command("capability")`. Keep all current
|
Register the existing body as `@app.command("capability")`. Keep all current
|
||||||
capability options and call `context.handlers.add_step_from_capability` with
|
capability options and call `context.handlers.add_step_from_capability` with
|
||||||
@@ -534,7 +534,7 @@ fields, parsed routes, input map, and output bindings exactly as the removed
|
|||||||
command does. Its docstring must state that it also projects schemas/bindings
|
command does. Its docstring must state that it also projects schemas/bindings
|
||||||
and recommend `wf draft validate`.
|
and recommend `wf draft validate`.
|
||||||
|
|
||||||
- [ ] **Step 5: Verify local and remote capability behavior**
|
- [x] **Step 5: Verify local and remote capability behavior**
|
||||||
|
|
||||||
Update old CLI tests from:
|
Update old CLI tests from:
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ wf draft add capability WORKSPACE --revision REVISION --step STEP --capability Q
|
|||||||
|
|
||||||
Keep assertions on request payload, projected schemas, route errors, and revision unchanged. Add a remote test proving it still calls `workflow.draft_workspaces.add_step_from_capability`, not generic insertion.
|
Keep assertions on request payload, projected schemas, route errors, and revision unchanged. Add a remote test proving it still calls `workflow.draft_workspaces.add_step_from_capability`, not generic insertion.
|
||||||
|
|
||||||
- [ ] **Step 6: Verify and commit**
|
- [x] **Step 6: Verify and commit**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -579,7 +579,7 @@ git commit -m "feat: group draft add commands"
|
|||||||
- Consumes: generic `add_step`, parsing helpers, and concrete draft models.
|
- Consumes: generic `add_step`, parsing helpers, and concrete draft models.
|
||||||
- Produces: four type-specific commands with local/remote parity.
|
- Produces: four type-specific commands with local/remote parity.
|
||||||
|
|
||||||
- [ ] **Step 1: Add a private command dispatcher and failing delegation tests**
|
- [x] **Step 1: Add a private command dispatcher and failing delegation tests**
|
||||||
|
|
||||||
Use one helper so every command has identical transport behavior:
|
Use one helper so every command has identical transport behavior:
|
||||||
|
|
||||||
@@ -611,7 +611,7 @@ def _submit_step(
|
|||||||
|
|
||||||
Tests must invoke both local fake handlers and `--url` RPC targets and assert the concrete model received by `add_step`.
|
Tests must invoke both local fake handlers and `--url` RPC targets and assert the concrete model received by `add_step`.
|
||||||
|
|
||||||
- [ ] **Step 2: Implement `interrupt` with schema and binding validation**
|
- [x] **Step 2: Implement `interrupt` with schema and binding validation**
|
||||||
|
|
||||||
Construct:
|
Construct:
|
||||||
|
|
||||||
@@ -640,7 +640,7 @@ DraftInterruptStep(interrupt=DraftInterruptPayload(
|
|||||||
|
|
||||||
Use the repository's existing binding payload/model helpers rather than duplicating path conversion. Tests cover two outcomes, both schemas, aliases, duplicate flags, malformed files, and no API call after parse failure.
|
Use the repository's existing binding payload/model helpers rather than duplicating path conversion. Tests cover two outcomes, both schemas, aliases, duplicate flags, malformed files, and no API call after parse failure.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement `foreach` and validate policy relationships**
|
- [x] **Step 3: Implement `foreach` and validate policy relationships**
|
||||||
|
|
||||||
Construct `DraftForeachPayload` from `--over`, `--as`, `--mode`, and:
|
Construct `DraftForeachPayload` from `--over`, `--as`, `--mode`, and:
|
||||||
|
|
||||||
@@ -658,11 +658,11 @@ concurrent = (
|
|||||||
|
|
||||||
Reject concurrent limits in serial mode with `typer.BadParameter`; rely on Pydantic to require `collect_to` for collect behavior. Route tests cover `loop`, `done`, and `completed_with_errors`.
|
Reject concurrent limits in serial mode with `typer.BadParameter`; rely on Pydantic to require `collect_to` for collect behavior. Route tests cover `loop`, `done`, and `completed_with_errors`.
|
||||||
|
|
||||||
- [ ] **Step 4: Implement `join` and `end`**
|
- [x] **Step 4: Implement `join` and `end`**
|
||||||
|
|
||||||
`join` constructs `DraftJoinStep(join={})` and accepts routes. `end` constructs `DraftEndStep(end=DraftEndPayload(outcome=outcome))`, exposes no `--route`, and passes `routes=None`.
|
`join` constructs `DraftJoinStep(join={})` and accepts routes. `end` constructs `DraftEndStep(end=DraftEndPayload(outcome=outcome))`, exposes no `--route`, and passes `routes=None`.
|
||||||
|
|
||||||
- [ ] **Step 5: Pin per-command help and error surfaces**
|
- [x] **Step 5: Pin per-command help and error surfaces**
|
||||||
|
|
||||||
For each command assert `--help` lists its own fields and does not list unrelated fields. Specifically:
|
For each command assert `--help` lists its own fields and does not list unrelated fields. Specifically:
|
||||||
|
|
||||||
@@ -671,7 +671,7 @@ For each command assert `--help` lists its own fields and does not list unrelate
|
|||||||
- join has only common routing flags;
|
- join has only common routing flags;
|
||||||
- end has `--outcome` but no `--route`.
|
- end has `--outcome` but no `--route`.
|
||||||
|
|
||||||
- [ ] **Step 6: Verify and commit**
|
- [x] **Step 6: Verify and commit**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -701,7 +701,7 @@ git commit -m "feat: add draft control step commands"
|
|||||||
- Consumes: `_submit_step`, JSON-file parsing, and draft models from prior tasks.
|
- Consumes: `_submit_step`, JSON-file parsing, and draft models from prior tasks.
|
||||||
- Produces: `when`, `choose`, `match`, and `subgraph` commands.
|
- Produces: `when`, `choose`, `match`, and `subgraph` commands.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing `when` tests and implement the command**
|
- [x] **Step 1: Write failing `when` tests and implement the command**
|
||||||
|
|
||||||
Given `condition.json`:
|
Given `condition.json`:
|
||||||
|
|
||||||
@@ -721,7 +721,7 @@ DraftWhenStep(when=DraftWhenPayload(
|
|||||||
|
|
||||||
The command must not expose `--route` because targets are embedded.
|
The command must not expose `--route` because targets are embedded.
|
||||||
|
|
||||||
- [ ] **Step 2: Write failing `choose` tests and implement the command**
|
- [x] **Step 2: Write failing `choose` tests and implement the command**
|
||||||
|
|
||||||
`--clauses-file` contains a JSON array. Validate with
|
`--clauses-file` contains a JSON array. Validate with
|
||||||
`TypeAdapter(list[DraftChooseClause]).validate_python(value)`, then construct
|
`TypeAdapter(list[DraftChooseClause]).validate_python(value)`, then construct
|
||||||
@@ -729,7 +729,7 @@ The command must not expose `--route` because targets are embedded.
|
|||||||
Tests cover ordered clauses, canonical `if` alias output, an empty array, a
|
Tests cover ordered clauses, canonical `if` alias output, an empty array, a
|
||||||
non-array document, and no generic routes.
|
non-array document, and no generic routes.
|
||||||
|
|
||||||
- [ ] **Step 3: Write failing `match` tests and implement the command**
|
- [x] **Step 3: Write failing `match` tests and implement the command**
|
||||||
|
|
||||||
`--cases-file` contains a JSON array. Validate with
|
`--cases-file` contains a JSON array. Validate with
|
||||||
`TypeAdapter(list[DraftMatchCase])`, then construct:
|
`TypeAdapter(list[DraftMatchCase])`, then construct:
|
||||||
@@ -744,7 +744,7 @@ DraftMatchStep(match=DraftMatchPayload(
|
|||||||
|
|
||||||
Tests preserve scalar `equals` values (`str`, `int`, `bool`, `None`) and ordered targets.
|
Tests preserve scalar `equals` values (`str`, `int`, `bool`, `None`) and ordered targets.
|
||||||
|
|
||||||
- [ ] **Step 4: Write failing subgraph reference tests**
|
- [x] **Step 4: Write failing subgraph reference tests**
|
||||||
|
|
||||||
Cover:
|
Cover:
|
||||||
|
|
||||||
@@ -756,7 +756,7 @@ Cover:
|
|||||||
|
|
||||||
All invalid combinations must fail before `add_step` is called.
|
All invalid combinations must fail before `add_step` is called.
|
||||||
|
|
||||||
- [ ] **Step 5: Implement subgraph construction**
|
- [x] **Step 5: Implement subgraph construction**
|
||||||
|
|
||||||
Build the reference explicitly:
|
Build the reference explicitly:
|
||||||
|
|
||||||
@@ -777,7 +777,7 @@ else:
|
|||||||
|
|
||||||
Then construct `DraftSubgraphPayload` with optional schema files, canonical input/output bindings, outcomes defaulting to `['ok']`, and description. Pass repeatable routes through `_submit_step`.
|
Then construct `DraftSubgraphPayload` with optional schema files, canonical input/output bindings, outcomes defaulting to `['ok']`, and description. Pass repeatable routes through `_submit_step`.
|
||||||
|
|
||||||
- [ ] **Step 6: Verify all nine commands and remote parity**
|
- [x] **Step 6: Verify all nine commands and remote parity**
|
||||||
|
|
||||||
Add a parameterized remote test that invokes every generic command and asserts method `workflow.draft_workspaces.add_step`, canonical step payload aliases, incoming route source, and routes. Keep capability in a separate assertion because it intentionally calls the composed method.
|
Add a parameterized remote test that invokes every generic command and asserts method `workflow.draft_workspaces.add_step`, canonical step payload aliases, incoming route source, and routes. Keep capability in a separate assertion because it intentionally calls the composed method.
|
||||||
|
|
||||||
@@ -789,7 +789,7 @@ uv run ruff check src/wf_cli/commands tests/wf_cli
|
|||||||
uv run basedpyright src/wf_cli/commands tests/wf_cli --level error
|
uv run basedpyright src/wf_cli/commands tests/wf_cli --level error
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
- [x] **Step 7: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add src/wf_cli/commands/draft_add.py tests/wf_cli
|
git add src/wf_cli/commands/draft_add.py tests/wf_cli
|
||||||
@@ -814,7 +814,7 @@ git commit -m "feat: add draft decision and subgraph commands"
|
|||||||
- Consumes: all implemented commands and method names.
|
- Consumes: all implemented commands and method names.
|
||||||
- Produces: accurate live docs and a clean, archived implementation record.
|
- Produces: accurate live docs and a clean, archived implementation record.
|
||||||
|
|
||||||
- [ ] **Step 1: Search live references before editing**
|
- [x] **Step 1: Search live references before editing**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -825,7 +825,7 @@ rg -n -F 'add_step_from_capability' docs skills --glob '!docs/historical/**'
|
|||||||
|
|
||||||
Classify each reference: migrate command examples; retain API references when they describe the composed capability helper; do not rewrite thesis/history solely for naming.
|
Classify each reference: migrate command examples; retain API references when they describe the composed capability helper; do not rewrite thesis/history solely for naming.
|
||||||
|
|
||||||
- [ ] **Step 2: Update user-facing CLI and skill documentation**
|
- [x] **Step 2: Update user-facing CLI and skill documentation**
|
||||||
|
|
||||||
Document the command tree and at least these complete examples:
|
Document the command tree and at least these complete examples:
|
||||||
|
|
||||||
@@ -846,11 +846,11 @@ wf draft add when report_ws --revision 3 --step decide \
|
|||||||
|
|
||||||
Explain that `when`/`choose`/`match` embed targets and do not accept `--route`, while invalid intermediate drafts remain saveable and should be checked with `wf draft validate`.
|
Explain that `when`/`choose`/`match` embed targets and do not accept `--route`, while invalid intermediate drafts remain saveable and should be checked with `wf draft validate`.
|
||||||
|
|
||||||
- [ ] **Step 3: Update API architecture and roadmap**
|
- [x] **Step 3: Update API architecture and roadmap**
|
||||||
|
|
||||||
Document `workflow.draft_workspaces.add_step`, the `DraftStep` boundary, separate map-key `step_id`, atomic route wiring, and the continued role of `add_step_from_capability`. Mark the roadmap slice complete only after verification.
|
Document `workflow.draft_workspaces.add_step`, the `DraftStep` boundary, separate map-key `step_id`, atomic route wiring, and the continued role of `add_step_from_capability`. Mark the roadmap slice complete only after verification.
|
||||||
|
|
||||||
- [ ] **Step 4: Resolve tracked issues honestly**
|
- [x] **Step 4: Resolve tracked issues honestly**
|
||||||
|
|
||||||
Change the three items in `ISSUES.md` to checked entries only if tests prove:
|
Change the three items in `ISSUES.md` to checked entries only if tests prove:
|
||||||
|
|
||||||
@@ -862,7 +862,7 @@ Change the three items in `ISSUES.md` to checked entries only if tests prove:
|
|||||||
|
|
||||||
Add any newly discovered out-of-scope defects as unchecked, reproducible statements.
|
Add any newly discovered out-of-scope defects as unchecked, reproducible statements.
|
||||||
|
|
||||||
- [ ] **Step 5: Run focused regression suites**
|
- [x] **Step 5: Run focused regression suites**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_adapter.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_cli/test_app.py tests/wf_cli/test_remote_target.py -q
|
uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_adapter.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_cli/test_app.py tests/wf_cli/test_remote_target.py -q
|
||||||
@@ -870,7 +870,7 @@ uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_ad
|
|||||||
|
|
||||||
Expected: all pass.
|
Expected: all pass.
|
||||||
|
|
||||||
- [ ] **Step 6: Run the repository quality gate**
|
- [x] **Step 6: Run the repository quality gate**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run ruff check
|
uv run ruff check
|
||||||
@@ -881,11 +881,11 @@ git diff --check
|
|||||||
|
|
||||||
If formatting fails, run `uv run ruff format`, inspect the diff, and rerun all four checks. Do not claim the full `uv run pytest -q` suite unless it is actually run; the focused matrix above is the required test gate for this slice.
|
If formatting fails, run `uv run ruff format`, inspect the diff, and rerun all four checks. Do not claim the full `uv run pytest -q` suite unless it is actually run; the focused matrix above is the required test gate for this slice.
|
||||||
|
|
||||||
- [ ] **Step 7: Review and archive**
|
- [x] **Step 7: Review and archive**
|
||||||
|
|
||||||
Run the `requesting-code-review` skill against the design/spec and this plan. Fix Critical/Important findings, rerun affected checks, tick completed plan checkboxes, then move the plan to the matching historical path and update live links.
|
Run the `requesting-code-review` skill against the design/spec and this plan. Fix Critical/Important findings, rerun affected checks, tick completed plan checkboxes, then move the plan to the matching historical path and update live links.
|
||||||
|
|
||||||
- [ ] **Step 8: Commit documentation and issue closure**
|
- [x] **Step 8: Commit documentation and issue closure**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add docs skills ISSUES.md
|
git add docs skills ISSUES.md
|
||||||
@@ -149,7 +149,7 @@ authoring intent.
|
|||||||
|
|
||||||
### Add Step From Capability
|
### Add Step From Capability
|
||||||
|
|
||||||
`add-step` atomically adds:
|
`add capability` atomically adds:
|
||||||
|
|
||||||
- one capability-backed `use` step;
|
- one capability-backed `use` step;
|
||||||
- explicit input bindings;
|
- explicit input bindings;
|
||||||
@@ -160,7 +160,7 @@ authoring intent.
|
|||||||
The outgoing CLI option is repeatable:
|
The outgoing CLI option is repeatable:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
wf draft add-step WORKSPACE `
|
wf draft add capability WORKSPACE `
|
||||||
--revision 4 `
|
--revision 4 `
|
||||||
--step second_echo `
|
--step second_echo `
|
||||||
--capability everything.default.echo `
|
--capability everything.default.echo `
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ helpers behave too much like final validation gates. In challenge runs, agents
|
|||||||
tried:
|
tried:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
wf draft add-step browser_click `
|
wf draft add capability browser_click `
|
||||||
--revision 1 `
|
--revision 1 `
|
||||||
--step wait `
|
--step wait `
|
||||||
--capability local.browser_click.wait_for_click `
|
--capability local.browser_click.wait_for_click `
|
||||||
@@ -66,7 +66,7 @@ Expected behavior:
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- A focused test proves `wf draft add-step --route ok=collect` persists an
|
- A focused test proves `wf draft add capability --route ok=collect` persists an
|
||||||
invalid workspace when `collect` does not exist yet.
|
invalid workspace when `collect` does not exist yet.
|
||||||
- A follow-up edit that adds `collect` can make the same workspace valid.
|
- A follow-up edit that adds `collect` can make the same workspace valid.
|
||||||
- `wf draft save` and `wf draft compile` continue to reject the invalid
|
- `wf draft save` and `wf draft compile` continue to reject the invalid
|
||||||
|
|||||||
@@ -169,8 +169,17 @@ frontends. Internally, they should prefer typed models from `wf_core`,
|
|||||||
`WorkflowDraftApi` owns draft workspace lifecycle, validation, compilation,
|
`WorkflowDraftApi` owns draft workspace lifecycle, validation, compilation,
|
||||||
JSON Patch application, and focused low-level map edits. `WorkflowDraftAuthoringApi`
|
JSON Patch application, and focused low-level map edits. `WorkflowDraftAuthoringApi`
|
||||||
is the semantic authoring layer above it: capability-aware bootstrap, bind,
|
is the semantic authoring layer above it: capability-aware bootstrap, bind,
|
||||||
add-step, branch, handle, and remove helpers lower intent into ordinary draft
|
typed step insertion, branch, handle, and remove helpers lower intent into
|
||||||
workspace patches while preserving revision checks.
|
ordinary draft workspace patches while preserving revision checks.
|
||||||
|
|
||||||
|
`workflow.draft_workspaces.add_step` accepts a discriminated `DraftStep` value,
|
||||||
|
while `step_id` remains the separate map key chosen by the caller. The operation
|
||||||
|
atomically inserts that step plus an optional incoming `RouteSource` and any
|
||||||
|
top-level outcome routes, so a failed structural check does not partially wire
|
||||||
|
the graph. Decision targets for `when`, `choose`, and `match` remain embedded in
|
||||||
|
their typed payloads. The composed `add_step_from_capability` operation remains
|
||||||
|
separate because it also resolves capability metadata, projects schemas, and
|
||||||
|
requires complete declared-outcome coverage.
|
||||||
|
|
||||||
## Relationship To wf_core
|
## Relationship To wf_core
|
||||||
|
|
||||||
|
|||||||
+44
-5
@@ -389,21 +389,27 @@ The command combines two common edits:
|
|||||||
|
|
||||||
Use `set-route` separately for outcome routing.
|
Use `set-route` separately for outcome routing.
|
||||||
|
|
||||||
### Add A Capability Step To A Draft
|
### Add Typed Steps To A Draft
|
||||||
|
|
||||||
Use `wf draft add-step` when adding a new capability-backed step
|
Use `wf draft add` to add one typed step to an existing draft:
|
||||||
to an existing draft. The command is explicit: it does not guess missing maps.
|
|
||||||
|
```text
|
||||||
|
capability interrupt foreach join end
|
||||||
|
when choose match subgraph
|
||||||
|
```
|
||||||
|
|
||||||
|
The capability command is explicit: it does not guess missing maps.
|
||||||
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings project
|
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings project
|
||||||
the corresponding workflow input/state schema fields from the capability input
|
the corresponding workflow input/state schema fields from the capability input
|
||||||
schema.
|
schema.
|
||||||
When the capability declares multiple outcomes, provide exactly one
|
When the capability declares multiple outcomes, provide exactly one
|
||||||
`--route OUTCOME=TARGET` for each declared outcome. Missing or unknown outcomes
|
`--route OUTCOME=TARGET` for each declared outcome. Missing or unknown outcomes
|
||||||
are rejected before the draft is mutated. When `add-step --route` rejects an
|
are rejected before the draft is mutated. When `add capability --route` rejects an
|
||||||
outcome, use the declared outcomes and repair text from the error. Remove
|
outcome, use the declared outcomes and repair text from the error. Remove
|
||||||
unknown route entries and add one route for each missing declared outcome.
|
unknown route entries and add one route for each missing declared outcome.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step report_ws \
|
wf draft add capability report_ws \
|
||||||
--revision 3 \
|
--revision 3 \
|
||||||
--step render \
|
--step render \
|
||||||
--capability local.report.render_markdown_report \
|
--capability local.report.render_markdown_report \
|
||||||
@@ -417,6 +423,39 @@ wf draft add-step report_ws \
|
|||||||
--bind-output title=state.title
|
--bind-output title=state.title
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Interrupts preserve explicit request and resume contracts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft add interrupt report_ws \
|
||||||
|
--revision 4 \
|
||||||
|
--step review \
|
||||||
|
--kind issue_review \
|
||||||
|
--request-schema-file request.schema.json \
|
||||||
|
--resume-schema-file resume.schema.json \
|
||||||
|
--outcome submitted \
|
||||||
|
--outcome cancelled \
|
||||||
|
--from-step draft_issues \
|
||||||
|
--from-outcome ok \
|
||||||
|
--route submitted=create_issues \
|
||||||
|
--route cancelled=revision_requested
|
||||||
|
```
|
||||||
|
|
||||||
|
Decision steps embed their targets and therefore do not accept `--route`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft add when report_ws \
|
||||||
|
--revision 5 \
|
||||||
|
--step decide \
|
||||||
|
--condition-file has-report.json \
|
||||||
|
--then publish \
|
||||||
|
--otherwise revise
|
||||||
|
```
|
||||||
|
|
||||||
|
`choose` reads an ordered clause array from `--clauses-file`; `match` reads an
|
||||||
|
ordered scalar case array from `--cases-file`. `subgraph` accepts either
|
||||||
|
`--workflow-name` or an immutable `--artifact-id` plus `--artifact-version`,
|
||||||
|
along with explicit boundary schemas and bindings.
|
||||||
|
|
||||||
Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
|
Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
|
||||||
mappings after a single flag; `--bind-output title=state.title
|
mappings after a single flag; `--bind-output title=state.title
|
||||||
summary=state.summary` is parsed as an unexpected extra argument.
|
summary=state.summary` is parsed as an unexpected extra argument.
|
||||||
|
|||||||
+12
-4
@@ -50,7 +50,9 @@ wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --
|
|||||||
wf draft compile <workspace_id>
|
wf draft compile <workspace_id>
|
||||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
|
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
|
||||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
|
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
|
||||||
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
||||||
|
wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
|
||||||
|
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ projection. Use `input/state -> local` for step inputs and `local ->
|
|||||||
state/output` for step outputs. It requires a capability-backed step with
|
state/output` for step outputs. It requires a capability-backed step with
|
||||||
`use`; use JSON Patch for non-capability/control draft steps.
|
`use`; use JSON Patch for non-capability/control draft steps.
|
||||||
|
|
||||||
To add a capability step, prefer `wf draft add-step` over raw
|
To add a capability step, prefer `wf draft add capability` over raw
|
||||||
JSON Patch when the route, input bindings, and output-to-state bindings are
|
JSON Patch when the route, input bindings, and output-to-state bindings are
|
||||||
known. It is explicit and does not guess missing maps.
|
known. It is explicit and does not guess missing maps.
|
||||||
If a capability has multiple outcomes, pass one `--route OUTCOME=TARGET` for
|
If a capability has multiple outcomes, pass one `--route OUTCOME=TARGET` for
|
||||||
@@ -114,9 +116,15 @@ Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
|
|||||||
mappings after one flag.
|
mappings after one flag.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
|
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use the matching `wf draft add <kind>` command for control steps. `when`,
|
||||||
|
`choose`, and `match` embed their targets and do not accept `--route`.
|
||||||
|
Interrupt and subgraph commands preserve their explicit schema contracts.
|
||||||
|
Intermediate drafts may remain `status: invalid`; run `wf draft validate`
|
||||||
|
after the intended steps and routes are present.
|
||||||
|
|
||||||
`wf draft compile` prints the raw plan JSON directly on success. Do not expect a
|
`wf draft compile` prints the raw plan JSON directly on success. Do not expect a
|
||||||
top-level `compiled_plan` key from the CLI output.
|
top-level `compiled_plan` key from the CLI output.
|
||||||
|
|
||||||
@@ -145,7 +153,7 @@ top-level `compiled_plan` key from the CLI output.
|
|||||||
- Do not use planning-session specs or implementation plans as user-facing runtime guidance.
|
- Do not use planning-session specs or implementation plans as user-facing runtime guidance.
|
||||||
- `set-input --map` is `GRAPH_SOURCE=BARE_LOCAL_FIELD`; never prefix the target
|
- `set-input --map` is `GRAPH_SOURCE=BARE_LOCAL_FIELD`; never prefix the target
|
||||||
with `local.`.
|
with `local.`.
|
||||||
- For `add-step --route`, route only outcomes reported by `wf cap inspect` or
|
- For `wf draft add capability --route`, route only outcomes reported by `wf cap inspect` or
|
||||||
the command error's `declared_outcomes` field.
|
the command error's `declared_outcomes` field.
|
||||||
- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
|
- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
|
||||||
raw plans use `nodes/edges/node`.
|
raw plans use `nodes/edges/node`.
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ If a patch returns `revision_conflict`, fetch the workspace again and retry
|
|||||||
against the latest revision.
|
against the latest revision.
|
||||||
|
|
||||||
Forward routes in drafts are allowed as invalid intermediate state. If
|
Forward routes in drafts are allowed as invalid intermediate state. If
|
||||||
`wf draft add-step --route ok=collect` returns `status: invalid`, add the
|
`wf draft add capability --route ok=collect` returns `status: invalid`, add the
|
||||||
missing `collect` step next, then run `wf draft validate`. Do not save or
|
missing `collect` step next, then run `wf draft validate`. Do not save or
|
||||||
compile until validation is valid.
|
compile until validation is valid.
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ Prefer focused helpers over JSON Patch for common edits:
|
|||||||
- `set_step_output_map`
|
- `set_step_output_map`
|
||||||
- `set_workflow_output_map`
|
- `set_workflow_output_map`
|
||||||
- `bind_draft`
|
- `bind_draft`
|
||||||
|
- `add_step`
|
||||||
- `add_step_from_capability`
|
- `add_step_from_capability`
|
||||||
- `branch_draft`
|
- `branch_draft`
|
||||||
- `handle_draft`
|
- `handle_draft`
|
||||||
@@ -109,7 +110,9 @@ wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --
|
|||||||
wf draft compile <workspace_id>
|
wf draft compile <workspace_id>
|
||||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
|
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
|
||||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
|
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
|
||||||
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
||||||
|
wf draft add interrupt <workspace_id> --revision <n> --step review --kind issue_review --request-schema-file request.schema.json --resume-schema-file resume.schema.json --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
|
||||||
|
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
|
||||||
```
|
```
|
||||||
|
|
||||||
`set-workflow-output` maps a graph source path (`input.*`, `state.*`, or
|
`set-workflow-output` maps a graph source path (`input.*`, `state.*`, or
|
||||||
@@ -163,7 +166,8 @@ wf draft validate <workspace_id>
|
|||||||
`--route OUTCOME=TARGET` for each outcome; when omitted and the capability
|
`--route OUTCOME=TARGET` for each outcome; when omitted and the capability
|
||||||
declares a single outcome, that outcome routes to `__end__`. Multi-outcome
|
declares a single outcome, that outcome routes to `__end__`. Multi-outcome
|
||||||
capabilities require exact route coverage; missing or unknown outcomes are
|
capabilities require exact route coverage; missing or unknown outcomes are
|
||||||
rejected before mutation. When `add-step --route` rejects an outcome, the
|
rejected before mutation. When `wf draft add capability --route` rejects an
|
||||||
|
outcome, the
|
||||||
error reports declared outcomes and direct add/remove repair guidance. Remove
|
error reports declared outcomes and direct add/remove repair guidance. Remove
|
||||||
unknown route entries and add one route for each missing declared outcome. It
|
unknown route entries and add one route for each missing declared outcome. It
|
||||||
still requires explicit choices; if you do not
|
still requires explicit choices; if you do not
|
||||||
@@ -173,7 +177,7 @@ wf draft validate <workspace_id>
|
|||||||
capability input schema.
|
capability input schema.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
|
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,6 +185,16 @@ Repeat `--input` and `--bind-output` once per mapping. Do not write
|
|||||||
`--bind-output title=state.title summary=state.summary`; the second mapping is
|
`--bind-output title=state.title summary=state.summary`; the second mapping is
|
||||||
an unexpected extra argument because it is not attached to its own flag.
|
an unexpected extra argument because it is not attached to its own flag.
|
||||||
|
|
||||||
|
- `add_step`
|
||||||
|
|
||||||
|
Adds any typed `DraftStep` with optional incoming and outgoing route wiring in
|
||||||
|
one revision. The CLI exposes one command per kind under `wf draft add`:
|
||||||
|
`interrupt`, `foreach`, `join`, `end`, `when`, `choose`, `match`, and
|
||||||
|
`subgraph`. Decision targets are embedded and reject `--route`. Interrupts
|
||||||
|
and subgraphs preserve JSON Schema boundary contracts. Invalid intermediate
|
||||||
|
drafts remain saveable in the workspace but must pass `wf draft validate`
|
||||||
|
before compile or artifact save.
|
||||||
|
|
||||||
- `branch_draft`
|
- `branch_draft`
|
||||||
|
|
||||||
Updates routes for an existing step in one revision without rewriting the
|
Updates routes for an existing step in one revision without rewriting the
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ validated, runnable deployment.
|
|||||||
local input/output property. It declares the matching schema and merges
|
local input/output property. It declares the matching schema and merges
|
||||||
the binding in one revision-checked edit.
|
the binding in one revision-checked edit.
|
||||||
- When adding a new capability-backed step, prefer:
|
- When adding a new capability-backed step, prefer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step ...
|
wf draft add capability ...
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
```
|
```
|
||||||
Use raw `wf draft patch` only when changing structure that no focused helper
|
|
||||||
covers.
|
For control flow, use the corresponding typed `wf draft add <kind>`
|
||||||
|
command. Use raw `wf draft patch` only when changing structure that no
|
||||||
|
focused helper covers.
|
||||||
- Use JSON Patch only for general structural edits.
|
- Use JSON Patch only for general structural edits.
|
||||||
6. Save an artifact.
|
6. Save an artifact.
|
||||||
- Draft artifact:
|
- Draft artifact:
|
||||||
|
|||||||
@@ -166,7 +166,13 @@ class WorkflowDraftAuthoringApi:
|
|||||||
routes: dict[str, str] | None = None,
|
routes: dict[str, str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Add one typed draft step and optional route edits in one revision."""
|
"""Add one typed draft step and optional route edits in one revision."""
|
||||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
checked = self._workspace_if_revision_matches(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
)
|
||||||
|
if isinstance(checked, dict):
|
||||||
|
return checked
|
||||||
|
workspace = checked
|
||||||
steps = workspace.draft.get("steps")
|
steps = workspace.draft.get("steps")
|
||||||
if not isinstance(steps, dict):
|
if not isinstance(steps, dict):
|
||||||
raise ValueError("draft steps must be an object")
|
raise ValueError("draft steps must be an object")
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
|||||||
"outcomes": step.interrupt.outcomes,
|
"outcomes": step.interrupt.outcomes,
|
||||||
}
|
}
|
||||||
if step.interrupt.request_schema is not None:
|
if step.interrupt.request_schema is not None:
|
||||||
interrupt_kwargs["request_schema"] = step.interrupt.request_schema.model_dump(
|
interrupt_kwargs["request_schema"] = (
|
||||||
mode="json", exclude_none=True
|
step.interrupt.request_schema.model_dump(mode="json", exclude_none=True)
|
||||||
)
|
)
|
||||||
if step.interrupt.resume_schema is not None:
|
if step.interrupt.resume_schema is not None:
|
||||||
interrupt_kwargs["resume_schema"] = step.interrupt.resume_schema.model_dump(
|
interrupt_kwargs["resume_schema"] = step.interrupt.resume_schema.model_dump(
|
||||||
|
|||||||
@@ -205,9 +205,7 @@ class DraftSubgraphPayload(BaseModel):
|
|||||||
workflow: WorkflowRef
|
workflow: WorkflowRef
|
||||||
desc: str | None = None
|
desc: str | None = None
|
||||||
input_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
|
input_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
|
||||||
output_schema: SchemaRef = Field(
|
output_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
|
||||||
default_factory=lambda: SchemaRef(type="object")
|
|
||||||
)
|
|
||||||
input: list[InputBinding] = Field(default_factory=list)
|
input: list[InputBinding] = Field(default_factory=list)
|
||||||
output: list[OutputBinding] = Field(default_factory=list)
|
output: list[OutputBinding] = Field(default_factory=list)
|
||||||
outcomes: list[str] = Field(default_factory=lambda: ["ok"], min_length=1)
|
outcomes: list[str] = Field(default_factory=lambda: ["ok"], min_length=1)
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ def add_step_from_capability(
|
|||||||
This command does not guess missing maps. Pass the route and bindings you
|
This command does not guess missing maps. Pass the route and bindings you
|
||||||
want, then run `wf draft validate <workspace_id>`.
|
want, then run `wf draft validate <workspace_id>`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
`wf draft add capability report_ws --revision 1 --step render
|
||||||
|
--capability local.report.render --route ok=__end__`
|
||||||
|
|
||||||
Repeat the flag for multiple bindings:
|
Repeat the flag for multiple bindings:
|
||||||
`--input state.title=title --input state.summary=summary`
|
`--input state.title=title --input state.summary=summary`
|
||||||
`--bind-output title=state.title --bind-output summary=state.summary`
|
`--bind-output title=state.title --bind-output summary=state.summary`
|
||||||
@@ -222,7 +226,11 @@ def add_interrupt_step(
|
|||||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a typed interrupt and its request/resume contract."""
|
"""Add a typed interrupt and its request/resume contract.
|
||||||
|
|
||||||
|
Example: `wf draft add interrupt WS --revision 1 --step review --kind review`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
request_map = _parse_step_input_map_flags(request, option_name="--request")
|
request_map = _parse_step_input_map_flags(request, option_name="--request")
|
||||||
resume_map = _parse_output_map_flags(resume, option_name="--resume")
|
resume_map = _parse_output_map_flags(resume, option_name="--resume")
|
||||||
routes = _parse_route_flags(route)
|
routes = _parse_route_flags(route)
|
||||||
@@ -287,9 +295,7 @@ def add_foreach_step(
|
|||||||
over: Annotated[
|
over: Annotated[
|
||||||
str, typer.Option("--over", help="Graph path containing the item list.")
|
str, typer.Option("--over", help="Graph path containing the item list.")
|
||||||
],
|
],
|
||||||
as_: Annotated[
|
as_: Annotated[str, typer.Option("--as", help="Context key for the current item.")],
|
||||||
str, typer.Option("--as", help="Context key for the current item.")
|
|
||||||
],
|
|
||||||
mode: Annotated[
|
mode: Annotated[
|
||||||
Literal["serial", "concurrent"],
|
Literal["serial", "concurrent"],
|
||||||
typer.Option("--mode", help="Item admission mode."),
|
typer.Option("--mode", help="Item admission mode."),
|
||||||
@@ -324,7 +330,11 @@ def add_foreach_step(
|
|||||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a foreach loop with explicit item and concurrency policies."""
|
"""Add a foreach loop with explicit item and concurrency policies.
|
||||||
|
|
||||||
|
Example: `wf draft add foreach WS --revision 1 --step each --over state.items --as item`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
if mode == "serial" and (max_active is not None or max_outstanding is not None):
|
if mode == "serial" and (max_active is not None or max_outstanding is not None):
|
||||||
raise typer.BadParameter(
|
raise typer.BadParameter(
|
||||||
"--max-active and --max-outstanding require --mode concurrent"
|
"--max-active and --max-outstanding require --mode concurrent"
|
||||||
@@ -389,7 +399,11 @@ def add_join_step(
|
|||||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a join step."""
|
"""Add a join step.
|
||||||
|
|
||||||
|
Example: `wf draft add join WS --revision 1 --step joined --route done=__end__`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
_submit_step(
|
_submit_step(
|
||||||
ctx,
|
ctx,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
@@ -421,7 +435,11 @@ def add_end_step(
|
|||||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add an explicit terminal outcome step."""
|
"""Add an explicit terminal outcome step.
|
||||||
|
|
||||||
|
Example: `wf draft add end WS --revision 1 --step finish --outcome ok`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
step = DraftEndStep(end=DraftEndPayload(outcome=outcome))
|
step = DraftEndStep(end=DraftEndPayload(outcome=outcome))
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
@@ -461,7 +479,11 @@ def add_when_step(
|
|||||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a boolean decision whose targets are embedded in the step."""
|
"""Add a boolean decision whose targets are embedded in the step.
|
||||||
|
|
||||||
|
Example: `wf draft add when WS --revision 1 --step decide --condition-file condition.json --then next`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
condition = _condition_adapter.validate_python(
|
condition = _condition_adapter.validate_python(
|
||||||
parse_json_file(condition_file, option_name="--condition-file")
|
parse_json_file(condition_file, option_name="--condition-file")
|
||||||
@@ -508,7 +530,11 @@ def add_choose_step(
|
|||||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add an ordered first-true decision with embedded targets."""
|
"""Add an ordered first-true decision with embedded targets.
|
||||||
|
|
||||||
|
Example: `wf draft add choose WS --revision 1 --step decide --clauses-file clauses.json`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
clauses = _choose_clauses_adapter.validate_python(
|
clauses = _choose_clauses_adapter.validate_python(
|
||||||
parse_json_file(clauses_file, option_name="--clauses-file")
|
parse_json_file(clauses_file, option_name="--clauses-file")
|
||||||
@@ -554,7 +580,11 @@ def add_match_step(
|
|||||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add an ordered scalar match decision with embedded targets."""
|
"""Add an ordered scalar match decision with embedded targets.
|
||||||
|
|
||||||
|
Example: `wf draft add match WS --revision 1 --step decide --value state.status --cases-file cases.json`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
cases = _match_cases_adapter.validate_python(
|
cases = _match_cases_adapter.validate_python(
|
||||||
parse_json_file(cases_file, option_name="--cases-file")
|
parse_json_file(cases_file, option_name="--cases-file")
|
||||||
@@ -637,7 +667,11 @@ def add_subgraph_step(
|
|||||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||||
] = None,
|
] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a child-workflow boundary with an explicit reference and contract."""
|
"""Add a child-workflow boundary with an explicit reference and contract.
|
||||||
|
|
||||||
|
Example: `wf draft add subgraph WS --revision 1 --step child --workflow-name child`.
|
||||||
|
Run `wf draft validate WS` after editing.
|
||||||
|
"""
|
||||||
if workflow_name is not None:
|
if workflow_name is not None:
|
||||||
if artifact_id is not None or artifact_version is not None:
|
if artifact_id is not None or artifact_version is not None:
|
||||||
raise typer.BadParameter(
|
raise typer.BadParameter(
|
||||||
|
|||||||
@@ -553,9 +553,7 @@ def test_adapter_lowers_subgraph_step_without_resolving_artifact() -> None:
|
|||||||
"input_schema": input_schema,
|
"input_schema": input_schema,
|
||||||
"output_schema": output_schema,
|
"output_schema": output_schema,
|
||||||
"input": [{"target": "topic", "path": "state.topic"}],
|
"input": [{"target": "topic", "path": "state.topic"}],
|
||||||
"output": [
|
"output": [{"source": "report", "target": "state.report"}],
|
||||||
{"source": "report", "target": "state.report"}
|
|
||||||
],
|
|
||||||
"outcomes": ["ok", "error"],
|
"outcomes": ["ok", "error"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -932,6 +932,34 @@ async def test_add_step_routes_incoming_and_outgoing_edges_atomically(
|
|||||||
assert workspace["draft"]["routes"]["new_step"] == {"ok": "__end__"}
|
assert workspace["draft"]["routes"]["new_step"] == {"ok": "__end__"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_step_stale_revision_wins_over_content_preflight(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_stale")
|
||||||
|
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
api = WorkflowApi(authoring.context)
|
||||||
|
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||||
|
before = await draft_api.get_draft_workspace(
|
||||||
|
workspace_id="draft_ws", include_draft=True
|
||||||
|
)
|
||||||
|
step = TypeAdapter(DraftStep).validate_python({"join": {}})
|
||||||
|
|
||||||
|
result = await api.add_step(
|
||||||
|
workspace_id="draft_ws",
|
||||||
|
revision=2,
|
||||||
|
step_id="echo",
|
||||||
|
step=step,
|
||||||
|
)
|
||||||
|
|
||||||
|
after = await draft_api.get_draft_workspace(
|
||||||
|
workspace_id="draft_ws", include_draft=True
|
||||||
|
)
|
||||||
|
assert result["status"] == "conflict"
|
||||||
|
assert result["diagnostics"][0]["code"] == "revision_conflict"
|
||||||
|
assert after == before
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_step_adds_missing_incoming_route_parent_atomically(
|
async def test_add_step_adds_missing_incoming_route_parent_atomically(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|||||||
+32
-26
@@ -233,6 +233,7 @@ def test_wf_draft_add_capability_help_explains_explicit_wiring() -> None:
|
|||||||
assert "does not guess" in output
|
assert "does not guess" in output
|
||||||
assert "projects its schemas and bindings" in output
|
assert "projects its schemas and bindings" in output
|
||||||
assert "draft validate" in output
|
assert "draft validate" in output
|
||||||
|
assert "wf draft add capability report_ws" in output
|
||||||
assert "Repeat the flag" in output
|
assert "Repeat the flag" in output
|
||||||
assert "--input state.title=title --input state.summary=summary" in output
|
assert "--input state.title=title --input state.summary=summary" in output
|
||||||
assert (
|
assert (
|
||||||
@@ -521,9 +522,7 @@ def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
|
|||||||
assert end_result.exit_code == 0, end_result.output
|
assert end_result.exit_code == 0, end_result.output
|
||||||
assert calls[0]["step"].model_dump(mode="json") == {"join": {}}
|
assert calls[0]["step"].model_dump(mode="json") == {"join": {}}
|
||||||
assert calls[0]["routes"] == {"done": "finish"}
|
assert calls[0]["routes"] == {"done": "finish"}
|
||||||
assert calls[1]["step"].model_dump(mode="json") == {
|
assert calls[1]["step"].model_dump(mode="json") == {"end": {"outcome": "completed"}}
|
||||||
"end": {"outcome": "completed"}
|
|
||||||
}
|
|
||||||
assert calls[1]["routes"] is None
|
assert calls[1]["routes"] is None
|
||||||
|
|
||||||
|
|
||||||
@@ -667,7 +666,9 @@ def test_wf_draft_add_control_commands_reject_invalid_input_before_api_call(
|
|||||||
assert "--bind-output" not in duplicate_resume.output
|
assert "--bind-output" not in duplicate_resume.output
|
||||||
assert "Traceback" not in duplicate_resume.output
|
assert "Traceback" not in duplicate_resume.output
|
||||||
assert missing_collect_target.exit_code == 2
|
assert missing_collect_target.exit_code == 2
|
||||||
assert "collect item error policy requires collect_to" in missing_collect_target.output
|
assert (
|
||||||
|
"collect item error policy requires collect_to" in missing_collect_target.output
|
||||||
|
)
|
||||||
assert "Traceback" not in missing_collect_target.output
|
assert "Traceback" not in missing_collect_target.output
|
||||||
assert end_route.exit_code == 2
|
assert end_route.exit_code == 2
|
||||||
assert "No such option" in end_route.output
|
assert "No such option" in end_route.output
|
||||||
@@ -681,7 +682,9 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
|
|||||||
join = runner.invoke(app, ["draft", "add", "join", "--help"])
|
join = runner.invoke(app, ["draft", "add", "join", "--help"])
|
||||||
end = runner.invoke(app, ["draft", "add", "end", "--help"])
|
end = runner.invoke(app, ["draft", "add", "end", "--help"])
|
||||||
|
|
||||||
assert interrupt.exit_code == foreach.exit_code == join.exit_code == end.exit_code == 0
|
assert (
|
||||||
|
interrupt.exit_code == foreach.exit_code == join.exit_code == end.exit_code == 0
|
||||||
|
)
|
||||||
assert "--request-schema-file" in interrupt.output
|
assert "--request-schema-file" in interrupt.output
|
||||||
assert "--resume-schema-file" in interrupt.output
|
assert "--resume-schema-file" in interrupt.output
|
||||||
assert "--request" in interrupt.output
|
assert "--request" in interrupt.output
|
||||||
@@ -701,7 +704,9 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
|
|||||||
assert "--route" not in end.output
|
assert "--route" not in end.output
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_add_decisions_build_ordered_typed_steps(monkeypatch, tmp_path) -> None:
|
def test_wf_draft_add_decisions_build_ordered_typed_steps(
|
||||||
|
monkeypatch, tmp_path
|
||||||
|
) -> None:
|
||||||
calls: list[dict[str, Any]] = []
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
class FakeHandlers:
|
class FakeHandlers:
|
||||||
@@ -714,9 +719,7 @@ def test_wf_draft_add_decisions_build_ordered_typed_steps(monkeypatch, tmp_path)
|
|||||||
"wf_cli.commands.draft_add.load_cli_context", lambda _ctx: context
|
"wf_cli.commands.draft_add.load_cli_context", lambda _ctx: context
|
||||||
)
|
)
|
||||||
condition_file = tmp_path / "condition.json"
|
condition_file = tmp_path / "condition.json"
|
||||||
condition_file.write_text(
|
condition_file.write_text('{"op":"exists","path":"state.report"}', encoding="utf-8")
|
||||||
'{"op":"exists","path":"state.report"}', encoding="utf-8"
|
|
||||||
)
|
|
||||||
clauses_file = tmp_path / "clauses.json"
|
clauses_file = tmp_path / "clauses.json"
|
||||||
clauses_file.write_text(
|
clauses_file.write_text(
|
||||||
'[{"if":{"op":"exists","path":"state.report"},"then":"publish"},'
|
'[{"if":{"op":"exists","path":"state.report"},"then":"publish"},'
|
||||||
@@ -835,9 +838,7 @@ def test_wf_draft_add_decisions_reject_bad_files_and_generic_routes(
|
|||||||
object_cases = tmp_path / "object.json"
|
object_cases = tmp_path / "object.json"
|
||||||
object_cases.write_text("{}", encoding="utf-8")
|
object_cases.write_text("{}", encoding="utf-8")
|
||||||
condition_file = tmp_path / "condition.json"
|
condition_file = tmp_path / "condition.json"
|
||||||
condition_file.write_text(
|
condition_file.write_text('{"op":"exists","path":"state.report"}', encoding="utf-8")
|
||||||
'{"op":"exists","path":"state.report"}', encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
empty = runner.invoke(
|
empty = runner.invoke(
|
||||||
app,
|
app,
|
||||||
@@ -978,23 +979,15 @@ def test_wf_draft_add_subgraph_builds_name_and_artifact_contracts(
|
|||||||
|
|
||||||
assert named.exit_code == 0, named.output
|
assert named.exit_code == 0, named.output
|
||||||
assert artifact.exit_code == 0, artifact.output
|
assert artifact.exit_code == 0, artifact.output
|
||||||
named_payload = calls[0]["step"].model_dump(mode="json", by_alias=True)[
|
named_payload = calls[0]["step"].model_dump(mode="json", by_alias=True)["subgraph"]
|
||||||
"subgraph"
|
|
||||||
]
|
|
||||||
assert named_payload["workflow"] == {"name": "child_workflow"}
|
assert named_payload["workflow"] == {"name": "child_workflow"}
|
||||||
assert named_payload["desc"] == "Generate the child report."
|
assert named_payload["desc"] == "Generate the child report."
|
||||||
assert named_payload["input_schema"]["properties"] == {
|
assert named_payload["input_schema"]["properties"] == {"topic": {"type": "string"}}
|
||||||
"topic": {"type": "string"}
|
|
||||||
}
|
|
||||||
assert named_payload["output_schema"]["properties"] == {
|
assert named_payload["output_schema"]["properties"] == {
|
||||||
"report": {"type": "string"}
|
"report": {"type": "string"}
|
||||||
}
|
}
|
||||||
assert named_payload["input"] == [
|
assert named_payload["input"] == [{"target": "topic", "path": "state.topic"}]
|
||||||
{"target": "topic", "path": "state.topic"}
|
assert named_payload["output"] == [{"source": "report", "target": "state.report"}]
|
||||||
]
|
|
||||||
assert named_payload["output"] == [
|
|
||||||
{"source": "report", "target": "state.report"}
|
|
||||||
]
|
|
||||||
assert named_payload["outcomes"] == ["ok", "error"]
|
assert named_payload["outcomes"] == ["ok", "error"]
|
||||||
assert calls[0]["routes"] == {"ok": "publish", "error": "revise"}
|
assert calls[0]["routes"] == {"ok": "publish", "error": "revise"}
|
||||||
artifact_payload = calls[1]["step"].model_dump(mode="json", by_alias=True)[
|
artifact_payload = calls[1]["step"].model_dump(mode="json", by_alias=True)[
|
||||||
@@ -1024,7 +1017,14 @@ def test_wf_draft_add_subgraph_rejects_invalid_reference_combinations(
|
|||||||
invalid_refs = [
|
invalid_refs = [
|
||||||
[],
|
[],
|
||||||
["--workflow-name", " "],
|
["--workflow-name", " "],
|
||||||
["--workflow-name", "child", "--artifact-id", "saved", "--artifact-version", "1"],
|
[
|
||||||
|
"--workflow-name",
|
||||||
|
"child",
|
||||||
|
"--artifact-id",
|
||||||
|
"saved",
|
||||||
|
"--artifact-version",
|
||||||
|
"1",
|
||||||
|
],
|
||||||
["--artifact-id", "saved"],
|
["--artifact-id", "saved"],
|
||||||
["--artifact-version", "1"],
|
["--artifact-version", "1"],
|
||||||
]
|
]
|
||||||
@@ -1056,7 +1056,9 @@ def test_wf_draft_add_decision_and_subgraph_help_is_type_specific() -> None:
|
|||||||
match = runner.invoke(app, ["draft", "add", "match", "--help"])
|
match = runner.invoke(app, ["draft", "add", "match", "--help"])
|
||||||
subgraph = runner.invoke(app, ["draft", "add", "subgraph", "--help"])
|
subgraph = runner.invoke(app, ["draft", "add", "subgraph", "--help"])
|
||||||
|
|
||||||
assert when.exit_code == choose.exit_code == match.exit_code == subgraph.exit_code == 0
|
assert (
|
||||||
|
when.exit_code == choose.exit_code == match.exit_code == subgraph.exit_code == 0
|
||||||
|
)
|
||||||
assert "--condition-file" in when.output
|
assert "--condition-file" in when.output
|
||||||
assert "--then" in when.output
|
assert "--then" in when.output
|
||||||
assert "--route" not in when.output
|
assert "--route" not in when.output
|
||||||
@@ -1073,6 +1075,10 @@ def test_wf_draft_add_decision_and_subgraph_help_is_type_specific() -> None:
|
|||||||
assert "--output-schema-file" in subgraph.output
|
assert "--output-schema-file" in subgraph.output
|
||||||
assert "--route" in subgraph.output
|
assert "--route" in subgraph.output
|
||||||
|
|
||||||
|
for result in (when, choose, match, subgraph):
|
||||||
|
assert "Example:" in result.output
|
||||||
|
assert "draft validate WS" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_help_does_not_list_old_add_step_from_capability() -> None:
|
def test_wf_draft_help_does_not_list_old_add_step_from_capability() -> None:
|
||||||
result = runner.invoke(app, ["draft", "--help"])
|
result = runner.invoke(app, ["draft", "--help"])
|
||||||
|
|||||||
@@ -1393,7 +1393,9 @@ def test_wf_draft_add_capability_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
|||||||
assert "workflow.draft_workspaces.add_step" not in rpc_methods
|
assert "workflow.draft_workspaces.add_step" not in rpc_methods
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path) -> None:
|
def test_wf_draft_add_control_steps_use_generic_rpc_target(
|
||||||
|
monkeypatch, tmp_path
|
||||||
|
) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
_patch_rpc_client_to_server(monkeypatch, server)
|
_patch_rpc_client_to_server(monkeypatch, server)
|
||||||
rpc_methods: list[str] = []
|
rpc_methods: list[str] = []
|
||||||
@@ -1421,9 +1423,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
condition_file = tmp_path / "condition.json"
|
condition_file = tmp_path / "condition.json"
|
||||||
condition_file.write_text(
|
condition_file.write_text('{"op":"exists","path":"state.value"}', encoding="utf-8")
|
||||||
'{"op":"exists","path":"state.value"}', encoding="utf-8"
|
|
||||||
)
|
|
||||||
clauses_file = tmp_path / "clauses.json"
|
clauses_file = tmp_path / "clauses.json"
|
||||||
clauses_file.write_text(
|
clauses_file.write_text(
|
||||||
'[{"if":{"op":"exists","path":"state.value"},"then":"call"}]',
|
'[{"if":{"op":"exists","path":"state.value"},"then":"call"}]',
|
||||||
@@ -1586,9 +1586,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path
|
|||||||
{
|
{
|
||||||
"workflow": {"name": "child"},
|
"workflow": {"name": "child"},
|
||||||
"input": [{"target": "value", "path": "input.value"}],
|
"input": [{"target": "value", "path": "input.value"}],
|
||||||
"output": [
|
"output": [{"source": "value", "target": "state.child_value"}],
|
||||||
{"source": "value", "target": "state.child_value"}
|
|
||||||
],
|
|
||||||
"outcomes": ["ok"],
|
"outcomes": ["ok"],
|
||||||
},
|
},
|
||||||
{"ok": "__end__"},
|
{"ok": "__end__"},
|
||||||
|
|||||||
@@ -952,9 +952,7 @@ def test_add_draft_step_params_preserve_typed_step_json() -> None:
|
|||||||
"workspace_id": "ws",
|
"workspace_id": "ws",
|
||||||
"revision": 1,
|
"revision": 1,
|
||||||
"step_id": "child",
|
"step_id": "child",
|
||||||
"step": {
|
"step": {"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}},
|
||||||
"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert subgraph.model_dump(mode="json", by_alias=True)["step"]["subgraph"][
|
assert subgraph.model_dump(mode="json", by_alias=True)["step"]["subgraph"][
|
||||||
@@ -983,7 +981,10 @@ def test_add_draft_step_params_reject_invalid_kind_and_route_source() -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
for incoming in ({"step_id": "", "outcome": "ok"}, {"step_id": "call", "outcome": ""}):
|
for incoming in (
|
||||||
|
{"step_id": "", "outcome": "ok"},
|
||||||
|
{"step_id": "call", "outcome": ""},
|
||||||
|
):
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
AddDraftStepParams.model_validate(
|
AddDraftStepParams.model_validate(
|
||||||
{
|
{
|
||||||
@@ -1058,9 +1059,7 @@ async def test_rpc_draft_workspace_add_typed_step_round_trip(tmp_path) -> None:
|
|||||||
assert "bad" not in fetched["result"]["draft"]["steps"]
|
assert "bad" not in fetched["result"]["draft"]["steps"]
|
||||||
review = fetched["result"]["draft"]["steps"]["review"]["interrupt"]
|
review = fetched["result"]["draft"]["steps"]["review"]["interrupt"]
|
||||||
assert review["request_schema"]["type"] == "object"
|
assert review["request_schema"]["type"] == "object"
|
||||||
assert review["resume_schema"]["properties"]["selected"] == {
|
assert review["resume_schema"]["properties"]["selected"] == {"type": "array"}
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
|
async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
|
||||||
@@ -1105,9 +1104,7 @@ async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
|
|||||||
interrupt = fetched["result"]["draft"]["steps"]["pause"]["interrupt"]
|
interrupt = fetched["result"]["draft"]["steps"]["pause"]["interrupt"]
|
||||||
assert interrupt["request_schema"] is None
|
assert interrupt["request_schema"] is None
|
||||||
assert interrupt["resume_schema"] is None
|
assert interrupt["resume_schema"] is None
|
||||||
assert fetched["result"]["draft"]["steps"]["pause"] == {
|
assert fetched["result"]["draft"]["steps"]["pause"] == {"interrupt": interrupt}
|
||||||
"interrupt": interrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_diagnoses_source(tmp_path) -> None:
|
async def test_rpc_diagnoses_source(tmp_path) -> None:
|
||||||
|
|||||||
@@ -721,9 +721,7 @@ async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) ->
|
|||||||
(
|
(
|
||||||
"subgraph",
|
"subgraph",
|
||||||
TypeAdapter(DraftStep).validate_python(
|
TypeAdapter(DraftStep).validate_python(
|
||||||
{
|
{"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}}
|
||||||
"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
{"workflow": {"artifact_id": "child", "version": 2}},
|
{"workflow": {"artifact_id": "child", "version": 2}},
|
||||||
),
|
),
|
||||||
@@ -769,12 +767,12 @@ async def test_rpc_client_add_step_preserves_all_typed_variants(
|
|||||||
assert request["params"]["step"]["foreach"]["as"] == "item"
|
assert request["params"]["step"]["foreach"]["as"] == "item"
|
||||||
assert "as_" not in request["params"]["step"]["foreach"]
|
assert "as_" not in request["params"]["step"]["foreach"]
|
||||||
if step_id == "interrupt":
|
if step_id == "interrupt":
|
||||||
assert request["params"]["step"]["interrupt"]["request_schema"][
|
assert (
|
||||||
"type"
|
request["params"]["step"]["interrupt"]["request_schema"]["type"] == "object"
|
||||||
] == "object"
|
)
|
||||||
assert request["params"]["step"]["interrupt"]["resume_schema"][
|
assert (
|
||||||
"type"
|
request["params"]["step"]["interrupt"]["resume_schema"]["type"] == "object"
|
||||||
] == "object"
|
)
|
||||||
if step_id == "subgraph":
|
if step_id == "subgraph":
|
||||||
assert request["params"]["step"]["subgraph"]["workflow"] == {
|
assert request["params"]["step"]["subgraph"]["workflow"] == {
|
||||||
"artifact_id": "child",
|
"artifact_id": "child",
|
||||||
|
|||||||
Reference in New Issue
Block a user