refactor: remove placeholder join step

This commit is contained in:
lda
2026-09-05 17:47:57 +07:00 Verified
parent 043bda6a34
commit cccdbc0a47
42 changed files with 88 additions and 309 deletions
+3 -2
View File
@@ -895,8 +895,9 @@ _Avoid_: Job, invocation
- Missing reducers mean replace only within a single serial lineage. At a
**Barrier**, multiple writes to the same path without a reducer are conflicts,
not last-writer-wins replacements.
- Current `JoinNode` should not silently become a **Barrier**. It may be
repurposed later only through an explicit design pass.
- The pass-through `JoinNode` was removed. Future **Gather** behavior starts
from an explicit barrier contract rather than inheriting placeholder
semantics.
- The first **Scheduler Foundation** implementation should create ready/block/wake
seams only. **Lineage Isolation** and **Barrier** merge behavior are future
concurrent semantics, not first-pass behavior.
-14
View File
@@ -1312,17 +1312,6 @@
],
"type": "object"
},
"DraftJoinStep": {
"additionalProperties": false,
"description": "Draft step that emits the current core join node.",
"properties": {
"join": {
"additionalProperties": true,
"type": "object"
}
},
"type": "object"
},
"DraftMatchCase": {
"additionalProperties": false,
"description": "One ordered equality case in a draft match decision.",
@@ -6209,9 +6198,6 @@
{
"$ref": "#/components/schemas/DraftInterruptStep"
},
{
"$ref": "#/components/schemas/DraftJoinStep"
},
{
"$ref": "#/components/schemas/DraftEndStep"
},
@@ -14,9 +14,10 @@ without making multiple matching edges silently mean broadcast.
The scheduler, ready queue, blocked frames, lineage-local state views, and
reducer-aware barrier commits already support concurrent foreach and native
subgraphs. They do not yet define general graph-level fork/gather. The existing
`JoinNode` is only a day-one marker: it immediately emits `done` and neither
waits nor merges. Renaming it would falsely preserve semantics it never had.
subgraphs. They do not yet define general graph-level fork/gather. A former
`JoinNode` day-one marker immediately emitted `done`; it was removed after a
census found no real persisted workflows using it. `GatherNode` therefore does
not inherit placeholder semantics or a compatibility burden.
A cross-system semantics review supported keeping `NodeResult.output` separate
from its named domain `outcome`, keeping operational failure outside that
@@ -135,11 +136,10 @@ feature would require an operational signal to the owning foreach and an
explicit policy for already admitted concurrent items. No `BreakNode` is added
without that use case and policy.
The current `JoinNode` will not be silently upgraded. Before implementation we
will verify whether real persisted artifacts use it. With no real compatibility
obligation, remove it and introduce `GatherNode` cleanly. If persisted callers
exist, define an explicit migration rather than assigning barrier semantics to
old `join` payloads.
The placeholder `JoinNode` was removed rather than silently upgraded. The
repository has no real persisted artifacts outside tests, so `GatherNode` can
be introduced with its actual rendezvous and merge contract and no legacy wire
alias.
## Considered Options
@@ -169,8 +169,8 @@ ordinary back-edge to the owning foreach already expresses item return in the
canonical graph. Break and race semantics remain separate future policies
rather than additional meanings assigned to ordinary outcomes.
**Reuse or rename `JoinNode`.** Rejected as the default because the existing
node is a pass-through marker with no barrier contract.
**Reuse or rename the placeholder `JoinNode`.** Rejected and removed because
the node was a pass-through marker with no barrier contract.
## Consequences
@@ -206,7 +206,6 @@ node is a pass-through marker with no barrier contract.
continuation frame in each topology shape.
- The trace representation for waiting and merging without excessive internal
scheduler noise.
- Whether any real persisted artifact requires migration from `JoinNode`.
This ADR extends the lineage and barrier direction established by
[ADR-0002](0002-concurrent-foreach-policy-and-barrier-commits.md). It remains
+17 -15
View File
@@ -40,23 +40,12 @@ author -> validate -> save artifact -> deploy -> run -> inspect or resume
## Active runtime sequence
The next three slices build on the foreach control-region, scheduler, lineage,
and barrier foundations in this order.
The next three slices build on the foreach control-region, structured-context,
scheduler, lineage, and barrier foundations in this order.
### 1. Review and merge structured runtime context
### 1. Add a persisted run step budget
The implementation plan is ready and its feature branch is under review:
- [`structured runtime context design`](superpowers/specs/2026-09-04-structured-runtime-context-design.md)
- [`structured runtime context implementation plan`](historical/superpowers/plans/2026-09-04-structured-runtime-context.md)
This slice gives runtime code, expressions, validation, and authoring references
one model for run data and same-scope foreach activations. Subgraphs continue to
cross an explicit input boundary rather than inheriting a parent's context.
### 2. Add a persisted run step budget
After structured context is stable, implement the proposed run-wide limit:
Implement the proposed run-wide limit:
- [`run step budget design`](superpowers/specs/2026-09-04-run-step-budget-design.md)
@@ -64,6 +53,13 @@ The budget must cover every frame and subgraph scope in one run, survive
checkpoint and resume, and stop valid but non-terminating graph cycles with a
clear runtime failure.
### 2. Consolidate runtime identity resolution
Introduce one internal resolver for a frame, lineage, runtime scope, and
foreach activation environment. The resolver should validate the canonical
identity chain once so fork/gather code does not pass related identifiers
independently or repeat ownership walks.
### 3. Implement explicit fork and gather
Reuse the scheduler, activation, lineage, and reducer-aware barrier machinery:
@@ -140,12 +136,18 @@ The active sequence can assume these foundations:
- Native subgraph scopes and durable return to the parent node
- Concurrent foreach with activation barriers and reducer-aware lineage merges
- Validated foreach back-edges with one static control region per node use
- Structured runtime context shared by execution, expressions, validation, and
authoring references
- Removal of the pass-through `JoinNode`; future `GatherNode` starts with its
actual synchronization contract and no placeholder compatibility
- Durable stopped-run inspection and resume
- Python client reconstruction of capabilities, artifacts, deployments, and
runs through the API
The current foreach return contract is
[`foreach back-edge design`](superpowers/specs/2026-09-04-foreach-back-edge-design.md).
The current context contract is
[`structured runtime context`](superpowers/specs/2026-09-04-structured-runtime-context-design.md).
## Historical entry points
@@ -112,7 +112,6 @@ Exactly one step-kind key must be present. Allowed step-kind keys are:
- `use`
- `foreach`
- `interrupt`
- `join`
Zero kind keys or multiple kind keys are validation errors.
@@ -243,19 +242,6 @@ This lowers to the current core `ForeachNode`.
This lowers to the current core `InterruptNode`.
### `join`
```json
{
"join": {}
}
```
This lowers to the current core `JoinNode`.
`join` is not a reverse branch. It remains reserved for actual join/frame
semantics.
## Routes
Most ordinary edges should be authored through `routes`:
@@ -337,7 +323,8 @@ node_c.unreachable
-> runtime_error
```
This is not `join`. It is compressed declaration of several ordinary edges.
This is not synchronization. It is compressed declaration of several ordinary
edges.
Possible later surfaces:
@@ -366,7 +353,7 @@ That should be handled as its own pass, not smuggled into this MCP draft change.
Potential later core work:
- true graph-as-node / subgraph support
- meaningful join semantics
- explicit fork/gather semantics
- future START-edge support if `Workflow.start` changes
### Draft `route` Sugar
@@ -157,7 +157,6 @@ Example:
"SubgraphNode",
"ConditionNode",
"ForeachNode",
"JoinNode",
"EndNode",
"InterruptNode"
]
@@ -85,8 +85,8 @@ semantic operation produces one patch and consumes one revision.
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
model also represents `end`, `condition`, `interrupt`, `foreach`, `when`,
`choose`, `match`, and subgraph 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.
@@ -132,7 +132,6 @@ Node presentation distinguishes the existing core node kinds:
- condition;
- interrupt;
- foreach;
- join;
- end and other control nodes.
Each node shows its stable id and concise semantic label. Selecting a node opens
@@ -226,4 +225,3 @@ The slice is complete when:
7. stale requests and partial failures cannot corrupt newer selections;
8. the `lda_report_workflow` lifecycle is readable without scrolling raw JSON;
9. frontend tests, typecheck, production build, and optional live smoke pass.
@@ -56,7 +56,6 @@ deliberate authoring vocabulary that is later lowered by
- `DraftUseStep`
- `DraftForeachStep`
- `DraftInterruptStep`
- `DraftJoinStep`
- `DraftEndStep`
- `DraftWhenStep`
- `DraftChooseStep`
@@ -149,7 +148,6 @@ Declared top-level outcomes are:
- `foreach`: `loop`, `done`, plus `completed_with_errors` when the item-error
policy is `skip` or `collect`;
- `interrupt`: `interrupt.outcomes`;
- `join`: `done`;
- `subgraph`: `subgraph.outcomes`.
The capability helper remains distinct because it resolves a capability,
@@ -175,7 +173,7 @@ reject malformed or ambiguous step objects before dispatching to the API.
The Python RPC client implements the same method on `WorkflowApi`. Client and
server serialize steps with aliases so fields such as foreach `as` and when
`if` retain their canonical wire names. Round-trip tests cover all nine step
`if` retain their canonical wire names. Round-trip tests cover all eight step
variants, including interrupt schemas and subgraph workflow references.
## CLI Shape
@@ -186,7 +184,6 @@ Register a focused Typer application beneath `wf draft`:
wf draft add capability
wf draft add interrupt
wf draft add foreach
wf draft add join
wf draft add end
wf draft add when
wf draft add choose
@@ -210,7 +207,6 @@ Variant-specific options are:
`--resume LOCAL_SOURCE=STATE_TARGET`, and repeatable `--outcome`;
- `foreach`: `--over`, `--as`, `--mode`, `--item-error`, optional
`--collect-to`, `--max-active`, and `--max-outstanding`;
- `join`: no variant-specific options;
- `end`: `--outcome` and no `--route`;
- `when`: `--condition-file`, `--then`, and `--otherwise`;
- `choose`: `--clauses-file` containing the ordered clause array and
@@ -269,7 +265,7 @@ modules need them; avoid a broad CLI refactor.
### CLI
- `wf draft add --help` lists all nine commands.
- `wf draft add --help` lists all eight commands.
- Per-command help exposes only relevant options.
- Every command builds the expected `DraftStep`, incoming source, and routes.
- Invalid flag combinations fail before calling the API.
@@ -28,7 +28,7 @@ The remaining graph-authoring work is split into independently useful slices:
1. selected-step input and output dataflow;
2. workflow Input, State, and Outcomes contract projections;
3. explicit End authoring and a typed Add step palette;
4. typed interrupt, control, subgraph, foreach, and join forms; and
4. typed interrupt, control, subgraph, and foreach forms; and
5. direct graph gestures lowered through the same canonical mutations.
This document specifies only the first item.
@@ -56,7 +56,7 @@ Slice 6 includes:
Slice 6 excludes:
- explicit End-node creation;
- typed creation forms for interrupt, condition, subgraph, foreach, or join;
- typed creation forms for interrupt, condition, subgraph, or foreach;
- graph gesture binding by drawing edges;
- arbitrary schema inference from runtime values;
- renaming existing step ids;
@@ -140,7 +140,7 @@ independent source of truth once the operation is available.
Standard frame context currently includes values such as prior outcome, active
incoming edge, scope id, lineage id, and parent lineage id. Foreach iteration
frames additionally expose loop item, loop index, and the configured foreach
alias. Future fork/join features may add branch-scoped context through the same
alias. Future fork/gather features may add branch-scoped context through the same
inventory without changing high-level clients.
Context availability is computed by core/API code using workflow graph and
@@ -36,7 +36,7 @@ They are runtime policy, not workflow graph semantics.
**Step Attempt** is one admitted attempt to execute a selected workflow `Step`
in one frame. Node uses, conditions, foreach controllers, subgraph boundaries,
interrupt nodes, joins, and explicit end nodes all count.
interrupt nodes, and explicit end nodes all count.
**Step Number** is the one-based ordinal assigned to an admitted step attempt
within a run.
@@ -256,7 +256,7 @@ with the run.
### Core counting
- A budget of one admits exactly one step and denies the second.
- Node, condition, foreach, subgraph, interrupt, join, and explicit end steps
- Node, condition, foreach, subgraph, interrupt, and explicit end steps
count.
- A transition to legacy `END` does not create an extra attempt.
- Handler failure still consumes its admitted step.
+1 -1
View File
@@ -444,7 +444,7 @@ Use `set-route` separately for outcome routing.
Use `wf draft add` to add one typed step to an existing draft:
```text
capability interrupt foreach join end
capability interrupt foreach end
when choose match subgraph
```
+5 -16
View File
@@ -399,8 +399,8 @@ are part of the graph definition:
```
Static values are not path mappings. Use `{"target": ..., "value": ...}` for
literal JSON values. Invalid draft step shapes are rejected instead of silently
compiling to `join`.
literal JSON values. Invalid draft step shapes are rejected instead of being
silently replaced with a placeholder step.
Generated MCP tool wrappers are intentionally naive. They normally expose both
`ok` and `error` outcomes, because MCP tool calls can report transport/provider
@@ -513,16 +513,6 @@ Declares an explicit workflow terminal outcome.
Use explicit `end` steps for non-`ok` workflow outcomes. The legacy `__end__`
destination remains the shorthand for public workflow outcome `ok`.
### `join`
Joins control flow.
```json
{
"join": {}
}
```
### `when`
Creates one boolean decision step. The condition uses the same JSON shape as
@@ -643,10 +633,9 @@ interrupt, an end step, or a subgraph rather than a capability:
```bash
wf draft create report_ws --name report_workflow
wf draft add join report_ws --revision 1 --step gate --route done=finish
wf draft set-start report_ws --revision 2 --step gate
wf draft add end report_ws --revision 3 --step finish --outcome error
wf draft set-contract report_ws --revision 4 --outcome error
wf draft add end report_ws --revision 1 --step finish --outcome error
wf draft set-start report_ws --revision 2 --step finish
wf draft set-contract report_ws --revision 3 --outcome error
wf draft validate report_ws
```
@@ -255,7 +255,7 @@ an unexpected extra argument because it is not attached to its own flag.
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
`interrupt`, `foreach`, `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`
-3
View File
@@ -16,7 +16,6 @@ from wf_artifacts.drafts.models import (
DraftEndStep,
DraftForeachStep,
DraftInterruptStep,
DraftJoinStep,
DraftMatchStep,
DraftStep,
DraftSubgraphStep,
@@ -353,8 +352,6 @@ class WorkflowDraftAuthoringApi:
return outcomes
if isinstance(step, DraftInterruptStep):
return set(step.interrupt.outcomes)
if isinstance(step, DraftJoinStep):
return {"done"}
if isinstance(step, DraftSubgraphStep):
return set(step.subgraph.outcomes)
if isinstance(
-2
View File
@@ -11,7 +11,6 @@ from .models import (
DraftEndStep,
DraftForeachStep,
DraftInterruptStep,
DraftJoinStep,
DraftMatchCase,
DraftMatchStep,
DraftSubgraphStep,
@@ -27,7 +26,6 @@ __all__ = [
"DraftEndStep",
"DraftForeachStep",
"DraftInterruptStep",
"DraftJoinStep",
"DraftMatchCase",
"DraftMatchStep",
"DraftSubgraphStep",
+1 -6
View File
@@ -4,7 +4,7 @@ from typing import Any
from wf_authoring import WorkflowBuilder
from wf_authoring.dsl import PathExpr
from wf_core import JoinNode, SubgraphNode, Workflow
from wf_core import SubgraphNode, Workflow
from wf_core.paths import GraphSourcePath
from .models import (
@@ -12,7 +12,6 @@ from .models import (
DraftEndStep,
DraftForeachStep,
DraftInterruptStep,
DraftJoinStep,
DraftMatchStep,
DraftStep,
DraftSubgraphStep,
@@ -84,10 +83,6 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
return builder.interrupt(
**interrupt_kwargs,
)
if isinstance(step, DraftJoinStep):
node = JoinNode(id=step_id, type="join")
builder.nodes.append(node)
return node
if isinstance(step, DraftEndStep):
return builder.end(step.end.outcome, id=step_id)
if isinstance(step, DraftWhenStep):
-9
View File
@@ -220,14 +220,6 @@ class DraftSubgraphStep(BaseModel):
subgraph: DraftSubgraphPayload
class DraftJoinStep(BaseModel):
"""Draft step that emits the current core join node."""
model_config = ConfigDict(extra="forbid")
join: JsonObject = Field(default_factory=dict)
class DraftEndPayload(BaseModel):
"""Payload for one explicit workflow terminal outcome."""
@@ -323,7 +315,6 @@ DraftStep = (
DraftUseStep
| DraftForeachStep
| DraftInterruptStep
| DraftJoinStep
| DraftEndStep
| DraftWhenStep
| DraftChooseStep
+1 -9
View File
@@ -8,7 +8,6 @@ from wf_core import (
EndNode,
ForeachNode,
InterruptNode,
JoinNode,
NodeUse,
SubgraphNode,
)
@@ -16,14 +15,7 @@ from wf_core import (
from ..nodes import NodeSpec
StepRef: TypeAlias = (
str
| NodeUse
| SubgraphNode
| ConditionNode
| ForeachNode
| InterruptNode
| JoinNode
| EndNode
str | NodeUse | SubgraphNode | ConditionNode | ForeachNode | InterruptNode | EndNode
)
"""A reference to a step, which can be either a string id or a node object
that should be auto-used."""
-38
View File
@@ -16,7 +16,6 @@ from wf_artifacts.drafts.models import (
DraftForeachStep,
DraftInterruptPayload,
DraftInterruptStep,
DraftJoinStep,
DraftMatchCase,
DraftMatchPayload,
DraftMatchStep,
@@ -425,43 +424,6 @@ def add_foreach_step(
)
@app.command("join")
def add_join_step(
ctx: typer.Context,
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
revision: Annotated[
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
],
step_id: Annotated[str, typer.Option("--step", help="New draft step id.")],
from_step: Annotated[
str | None, typer.Option("--from-step", help="Incoming step id.")
] = None,
from_outcome: Annotated[
str | None,
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
] = None,
route: Annotated[
list[str] | None,
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
] = None,
) -> None:
"""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(
ctx,
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
step=DraftJoinStep(join={}),
from_step=from_step,
from_outcome=from_outcome,
routes=_parse_route_flags(route) or None,
)
@app.command("end")
def add_end_step(
ctx: typer.Context,
+1 -1
View File
@@ -56,7 +56,7 @@ def write_json_file(path: Path, payload: Any, *, force: bool) -> None:
output.write("\n")
except FileExistsError as exc:
raise CliInputError(
f"file {path!s} already exists; use --force to replace it"
f"file already exists: {path!s}; use --force to replace it"
) from exc
except OSError as exc:
raise CliInputError(f"could not write file {path!s}: {exc}") from exc
-2
View File
@@ -11,7 +11,6 @@ from .models import (
InputPathBinding,
InputValueBinding,
InterruptNode,
JoinNode,
JsonValue,
LiteralExpression,
NodeDef,
@@ -89,7 +88,6 @@ __all__ = [
"InputValueBinding",
"InterruptRequest",
"InterruptRoute",
"JoinNode",
"JsonValue",
"LiteralExpression",
"NodeDef",
-2
View File
@@ -30,7 +30,6 @@ from wf_core.models.steps import (
ForeachItemErrorPolicy,
ForeachNode,
InterruptNode,
JoinNode,
NodeUse,
Step,
SubgraphNode,
@@ -54,7 +53,6 @@ __all__ = [
"InputExpressionBinding",
"InputPathBinding",
"InputValueBinding",
"JoinNode",
"JsonValue",
"LiteralExpression",
"LiteralOperand",
+1 -14
View File
@@ -293,13 +293,6 @@ class ForeachNode(BaseModel):
return GraphSourcePath("context", ("foreach", self.id, "index"))
class JoinNode(BaseModel):
"""Control-flow step that marks a branch or frame as joined."""
id: str
type: Literal["join"]
class EndNode(BaseModel):
"""Explicit workflow terminal that sets the workflow-level outcome.
@@ -411,13 +404,7 @@ class InterruptNode(BaseModel):
Step = Annotated[
NodeUse
| SubgraphNode
| ConditionNode
| ForeachNode
| JoinNode
| EndNode
| InterruptNode,
NodeUse | SubgraphNode | ConditionNode | ForeachNode | EndNode | InterruptNode,
Field(discriminator="type"),
]
"""Discriminated union of all executable workflow graph steps."""
-9
View File
@@ -41,15 +41,6 @@ def handle_condition_step(
)
def handle_join_step() -> StepExecutionResult:
return StepExecutionResult(
outcome="done",
resolved_input={},
output={},
state_changes={},
)
def handle_interrupt_step(
run: RunState,
step: InterruptNode,
-6
View File
@@ -10,7 +10,6 @@ from wf_core.models.steps import (
EndNode,
ForeachNode,
InterruptNode,
JoinNode,
NodeUse,
SubgraphNode,
)
@@ -22,7 +21,6 @@ from wf_core.runtime.ops.foreach import step_foreach
from wf_core.runtime.ops.handlers import (
handle_condition_step,
handle_interrupt_step,
handle_join_step,
)
from wf_core.runtime.ops.index import WorkflowIndex, build_workflow_index
from wf_core.runtime.ops.merges import ReducerDefinition
@@ -149,8 +147,6 @@ def step_workflow(
raise
elif isinstance(step, ConditionNode):
step_result = handle_condition_step(run, step)
elif isinstance(step, JoinNode):
step_result = handle_join_step()
elif isinstance(step, EndNode):
return complete_end_step(
run=run,
@@ -264,8 +260,6 @@ async def step_workflow_async(
raise
elif isinstance(step, ConditionNode):
step_result = handle_condition_step(run, step)
elif isinstance(step, JoinNode):
step_result = handle_join_step()
elif isinstance(step, EndNode):
return complete_end_step(
run=run,
-2
View File
@@ -19,8 +19,6 @@ def declared_outcomes_for_step(step: Step, node_defs: dict[str, NodeDef]) -> set
if step.item_error.action in {"skip", "collect"}:
outcomes.add("completed_with_errors")
return outcomes
if step.type == "join":
return {"done"}
if isinstance(step, EndNode):
return set()
if isinstance(step, InterruptNode):
+1 -1
View File
@@ -226,7 +226,7 @@ def test_draft_step_requires_exactly_one_kind_key() -> None:
assert isinstance(steps, dict)
echo = steps["echo"]
assert isinstance(echo, dict)
echo["join"] = {}
echo["end"] = {"outcome": "ok"}
with pytest.raises(ValidationError) as exc_info:
WorkflowDraft.model_validate(draft)
+5 -6
View File
@@ -675,7 +675,7 @@ async def test_update_capability_step_rejects_wrong_kind_and_invalid_input_atomi
register_echo=True,
)
draft = _echo_draft()
draft["steps"]["joined"] = {"join": ["echo"]}
draft["steps"]["joined"] = {"end": {}}
await draft_api.create_draft_workspace(workspace_id="echo", draft=draft)
before = await draft_api.get_draft_workspace(
workspace_id="echo",
@@ -3136,7 +3136,7 @@ async def test_set_step_output_bindings_rejects_non_capability_step_without_muta
await draft_api.patch_draft_workspace(
workspace_id="non-capability-output-step",
revision=1,
patch=[{"op": "replace", "path": "/steps/render", "value": {"join": {}}}],
patch=[{"op": "replace", "path": "/steps/render", "value": {"end": {}}}],
)
before = await draft_api.get_draft_workspace(
workspace_id="non-capability-output-step",
@@ -3246,7 +3246,7 @@ async def test_set_step_output_bindings_stale_revision_precedes_non_capability_s
await draft_api.patch_draft_workspace(
workspace_id="stale-output-non-capability",
revision=1,
patch=[{"op": "replace", "path": "/steps/render", "value": {"join": {}}}],
patch=[{"op": "replace", "path": "/steps/render", "value": {"end": {}}}],
)
before = await draft_api.get_draft_workspace(
workspace_id="stale-output-non-capability",
@@ -3504,7 +3504,7 @@ async def test_set_step_input_bindings_rejects_non_capability_step_without_mutat
register_echo=True,
)
draft = _structured_report_draft()
draft["steps"]["report"] = {"join": {}}
draft["steps"]["report"] = {"end": {}}
await draft_api.create_draft_workspace(workspace_id="non_capability", draft=draft)
api = WorkflowApi(authoring.context, drafts=True)
before = await draft_api.get_draft_workspace(
@@ -3956,7 +3956,6 @@ async def test_add_step_from_capability_rejects_existing_step_id(
"interrupt",
{"interrupt": {"kind": "review", "outcomes": ["submitted"]}},
),
("join", {"join": {}}),
("end", {"end": {"outcome": "ok"}}),
(
"when",
@@ -4067,7 +4066,7 @@ async def test_add_step_stale_revision_wins_over_content_preflight(
before = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
)
step = TypeAdapter(DraftStep).validate_python({"join": {}})
step = TypeAdapter(DraftStep).validate_python({"end": {}})
result = await api.add_step(
workspace_id="draft_ws",
+5 -34
View File
@@ -824,7 +824,6 @@ def test_wf_draft_add_help_lists_typed_step_commands_and_removes_flat_command()
"capability",
"interrupt",
"foreach",
"join",
"end",
"when",
"choose",
@@ -1531,7 +1530,7 @@ def test_wf_draft_add_foreach_builds_concurrent_policy(monkeypatch) -> None:
assert calls[1]["step"].foreach.concurrent.max_outstanding == 20
def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
def test_wf_draft_add_end_builds_concrete_step(monkeypatch) -> None:
calls: list[dict[str, Any]] = []
class FakeHandlers:
@@ -1544,25 +1543,6 @@ def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
"wf_cli.commands.draft_add.load_cli_context", lambda _ctx: context
)
join_result = runner.invoke(
app,
[
"draft",
"add",
"join",
"workspace",
"--revision",
"1",
"--step",
"joined",
"--from-step",
"each_issue",
"--from-outcome",
"done",
"--route",
"done=finish",
],
)
end_result = runner.invoke(
app,
[
@@ -1571,7 +1551,7 @@ def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
"end",
"workspace",
"--revision",
"2",
"1",
"--step",
"finish",
"--outcome",
@@ -1579,12 +1559,9 @@ def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
],
)
assert join_result.exit_code == 0, join_result.output
assert end_result.exit_code == 0, end_result.output
assert calls[0]["step"].model_dump(mode="json") == {"join": {}}
assert calls[0]["routes"] == {"done": "finish"}
assert calls[1]["step"].model_dump(mode="json") == {"end": {"outcome": "completed"}}
assert calls[1]["routes"] is None
assert calls[0]["step"].model_dump(mode="json") == {"end": {"outcome": "completed"}}
assert calls[0]["routes"] is None
def test_wf_draft_add_control_commands_reject_invalid_input_before_api_call(
@@ -1788,12 +1765,9 @@ def test_wf_draft_add_control_commands_reject_invalid_input_before_api_call(
def test_wf_draft_add_control_command_help_is_type_specific() -> None:
interrupt = runner.invoke(app, ["draft", "add", "interrupt", "--help"])
foreach = runner.invoke(app, ["draft", "add", "foreach", "--help"])
join = runner.invoke(app, ["draft", "add", "join", "--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 == end.exit_code == 0
assert "--request-schema-file" in interrupt.output
assert "--resume-schema-file" in interrupt.output
assert "--request" in interrupt.output
@@ -1806,9 +1780,6 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
assert "--item-error" in foreach.output
assert "--collect-to" in foreach.output
assert "--request-schema-file" not in foreach.output
assert "--from-step" in join.output
assert "--route" in join.output
assert "--request-schema-file" not in join.output
assert "--outcome" in end.output
assert "--route" not in end.output
+8 -21
View File
@@ -771,14 +771,14 @@ def test_wf_remote_capability_free_draft_lifecycle(monkeypatch, tmp_path) -> Non
[
"draft",
"add",
"join",
"end",
"control_ws",
"--revision",
"1",
"--step",
"gate",
"--route",
"done=finish",
"finish",
"--outcome",
"error",
],
[
"draft",
@@ -787,26 +787,14 @@ def test_wf_remote_capability_free_draft_lifecycle(monkeypatch, tmp_path) -> Non
"--revision",
"2",
"--step",
"gate",
],
[
"draft",
"add",
"end",
"control_ws",
"--revision",
"3",
"--step",
"finish",
"--outcome",
"error",
],
[
"draft",
"set-contract",
"control_ws",
"--revision",
"4",
"3",
"--outcome",
"error",
],
@@ -824,10 +812,10 @@ def test_wf_remote_capability_free_draft_lifecycle(monkeypatch, tmp_path) -> Non
assert '"status": "valid"' in results[-1].output
assert inspected.exit_code == 0, inspected.output
payload = json.loads(inspected.output)
assert payload["revision"] == 5
assert payload["draft"]["start"] == "gate"
assert payload["revision"] == 4
assert payload["draft"]["start"] == "finish"
assert payload["draft"]["outcomes"] == ["error"]
assert set(payload["draft"]["steps"]) == {"gate", "finish"}
assert set(payload["draft"]["steps"]) == {"finish"}
def test_wf_draft_export_uses_remote_get_and_writes_only_draft(
@@ -2409,7 +2397,6 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(
"completed_with_errors": "__end__",
},
),
("join", ["--route", "done=__end__"], {}, {"done": "__end__"}),
("end", ["--outcome", "ok"], {"outcome": "ok"}, None),
(
"when",
-1
View File
@@ -67,7 +67,6 @@ def test_schema_compact_alias_is_json_outline_without_refs() -> None:
"SubgraphNode",
"ConditionNode",
"ForeachNode",
"JoinNode",
"EndNode",
"InterruptNode",
]
+1 -1
View File
@@ -92,7 +92,7 @@ def test_generates_the_complete_real_workflow_contract() -> None:
assert len(manifest["operations"]) == 72
assert len({operation["method"] for operation in manifest["operations"]}) == 72
assert len(schemas) == 142
assert len(schemas) == 141
assert len(manifest["components"]["errors"]) == 1
assert all(
set(operation["result"]["schema"]) == {"$ref"}
+2 -2
View File
@@ -2090,7 +2090,7 @@ def test_add_draft_step_params_reject_invalid_kind_and_route_source() -> None:
"workspace_id": "ws",
"revision": 1,
"step_id": "bad",
"step": {"use": "demo.echo", "join": {}},
"step": {"use": "demo.echo", "end": {}},
}
)
@@ -2104,7 +2104,7 @@ def test_add_draft_step_params_reject_invalid_kind_and_route_source() -> None:
"workspace_id": "ws",
"revision": 1,
"step_id": "new",
"step": {"join": {}},
"step": {"end": {}},
"incoming": incoming,
}
)
+13 -26
View File
@@ -13,7 +13,6 @@ from wf_api.surface import RouteSource, WorkflowDraftSurface
from wf_artifacts.drafts.models import (
DraftEndPayload,
DraftEndStep,
DraftJoinStep,
DraftStep,
)
from wf_core import END
@@ -573,32 +572,25 @@ async def test_rpc_client_builds_capability_free_draft_lifecycle(tmp_path) -> No
workspace_id="control_first",
name="control_first",
)
joined = await client.add_step(
workspace_id="control_first",
revision=created["revision"],
step_id="gate",
step=DraftJoinStep(join={}),
routes={"done": "finish"},
)
started = await client.set_draft_start(
workspace_id="control_first",
revision=joined["revision"],
step_id="gate",
)
ended = await client.add_step(
workspace_id="control_first",
revision=started["revision"],
revision=created["revision"],
step_id="finish",
step=DraftEndStep(end=DraftEndPayload(outcome="error")),
)
contracted = await client.set_draft_contract(
started = await client.set_draft_start(
workspace_id="control_first",
revision=ended["revision"],
step_id="finish",
)
contracted = await client.set_draft_contract(
workspace_id="control_first",
revision=started["revision"],
outcomes=("error",),
)
stale = await client.set_draft_start(
workspace_id="control_first",
revision=ended["revision"],
revision=started["revision"],
step_id="finish",
)
validated = await client.validate_draft_workspace(workspace_id="control_first")
@@ -609,22 +601,18 @@ async def test_rpc_client_builds_capability_free_draft_lifecycle(tmp_path) -> No
)
assert created["revision"] == 1
assert joined["revision"] == 2
assert ended["revision"] == 2
assert started["revision"] == 3
assert ended["revision"] == 4
assert contracted["revision"] == 5
assert contracted["revision"] == 4
assert stale["status"] == "conflict"
assert stale["diagnostics"][0]["code"] == "revision_conflict"
assert validated["status"] == "valid"
assert "compiled_plan" in compiled
assert compiled["compiled_plan"]["start"] == "gate"
assert compiled["compiled_plan"]["start"] == "finish"
draft = inspected.get("draft")
assert draft is not None
assert draft["start"] == "gate"
assert draft["steps"] == {
"gate": {"join": {}},
"finish": {"end": {"outcome": "error"}},
}
assert draft["start"] == "finish"
assert draft["steps"] == {"finish": {"end": {"outcome": "error"}}}
def test_rpc_client_satisfies_draft_surface_static_shape() -> None:
@@ -1226,7 +1214,6 @@ async def test_rpc_client_preserves_nested_local_path_strings() -> None:
"outcomes": ["submitted"],
},
),
("join", TypeAdapter(DraftStep).validate_python({"join": {}}), {}),
("end", TypeAdapter(DraftStep).validate_python({"end": {}}), {"outcome": "ok"}),
(
"when",
@@ -31,8 +31,6 @@ const nodeColor = (data: WorkflowGraphNodeData): string => {
return "#ef4444";
case "foreach":
return "#8b5cf6";
case "join":
return "#10b981";
case "end":
return "#6b7280";
default:
@@ -7,7 +7,6 @@ export type WorkflowGraphNodeKind =
| "condition"
| "interrupt"
| "foreach"
| "join"
| "end"
| "unsupported";
@@ -94,8 +93,6 @@ const mapNodeKind = (type: unknown): WorkflowGraphNodeKind => {
return "interrupt";
case "foreach":
return "foreach";
case "join":
return "join";
case "end":
return "end";
default:
@@ -119,7 +116,6 @@ const buildLabel = (
return typeof node.kind === "string" ? node.kind : "Interrupt";
}
if (type === "foreach") return "For Each";
if (type === "join") return "Join";
if (type === "subgraph") {
const workflowRef = typeof node.workflow === "string" ? node.workflow : undefined;
if (workflowRef) {
@@ -46,7 +46,6 @@ describe("architectureCatalog", () => {
"NodeUse",
"Condition",
"Foreach",
"Join",
"Subgraph",
"Interrupt",
"End",
@@ -524,7 +524,6 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
"node-use": { x: 0, y: 0 },
"condition": { x: 300, y: 0 },
"foreach": { x: 600, y: 0 },
"join": { x: 900, y: 0 },
"subgraph": { x: 150, y: 210 },
"interrupt": { x: 450, y: 210 },
"end": { x: 750, y: 210 },
@@ -542,7 +541,6 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
},
{ id: "condition", label: "Condition", summary: "Route true or false", kind: "decision", icon: "branch", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "foreach", label: "Foreach", summary: "Create item frames and barrier", kind: "loop", icon: "repeat", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "join", label: "Join", summary: "Close a branch or frame", kind: "operation", icon: "workflow", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "subgraph", label: "Subgraph", summary: "Enter a prepared child workflow", kind: "boundary", icon: "layers", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "interrupt", label: "Interrupt", summary: "Persist request and wait", kind: "boundary", icon: "pause", evidencePointer: "src/wf_core/models/steps.py", childFigureId: "interrupt-contract-detail" },
{ id: "end", label: "End", summary: "Project a terminal outcome", kind: "terminal", icon: "stop", evidencePointer: "src/wf_core/models/steps.py" },
@@ -57,7 +57,6 @@ const stepKind = (step: JsonRecord): string => {
"choose",
"match",
"foreach",
"join",
"end",
]) {
if (kind in step) return kind;
@@ -464,7 +464,6 @@ export interface WorkflowContractMap {
| DraftUseStep
| DraftForeachStep
| DraftInterruptStep
| DraftJoinStep
| DraftEndStep
| DraftWhenStep
| DraftChooseStep
@@ -1867,17 +1866,6 @@ export interface SchemaRef {
type?: string | string[] | null;
[k: string]: unknown;
}
/**
* Draft step that emits the current core join node.
*
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "DraftJoinStep".
*/
export interface DraftJoinStep {
join?: {
[k: string]: unknown;
};
}
/**
* Draft step that lowers to core `EndNode`.
*