Files
lda-wf/docs/superpowers/specs/2026-05-19-authoring-control-flow-design.md
T
2026-05-19 03:07:37 +07:00

224 lines
5.0 KiB
Markdown

# Authoring Control Flow Design
## Goal
Make `wf_authoring` control flow precise enough that future sugar layers can
delegate to it instead of reimplementing graph semantics.
The current problem is `route()`: it accepts both `PathExpr` and `Expr`, then
dispatches to two different behaviors. Both "route" something, but the caller
must understand different case semantics depending on the argument type.
That is too blurry for the core authoring API.
## Design Rule
Each public control-flow method should name one decision mechanism.
| Method | Decision source | Meaning |
| --- | --- | --- |
| `branch` | a node's declared outcome | wire outcome labels to targets |
| `match` | one graph value | compare that value against equality cases |
| `when` | one boolean condition | route through `true` / `false` |
| `choose` | ordered boolean conditions | first true condition wins |
| `handle` | several source/outcome pairs | send shared outcomes to one target |
Future fluent builders or operator sugar must call these methods rather than
constructing edges/conditions independently.
## Keep `branch`
`branch()` is already good:
```python
g.branch(tool, {
"ok": next_step,
"error": fail_step,
})
```
It has one clear meaning:
```text
route an existing node outcome
```
It should remain named `branch`. Alternatives such as `on_outcome` are more
literal but worse to use, and the current method does not suffer from the
semantic overloading that `route()` does.
## Replace `route`
### `match`
```python
g.match(
state("status"),
{
"done": finish,
"retry": retry,
},
default=fail,
)
```
Meaning:
```text
if state.status == "done": finish
elif state.status == "retry": retry
else: fail
```
This is the existing `PathExpr` branch of `route()`, renamed to say what it
actually does.
### `when`
```python
g.when(
state("count").ge(1),
then=positive,
otherwise=zero,
)
```
Meaning:
```text
if state.count >= 1: positive
else: zero
```
This is the existing `Expr` branch of `route()`, made explicit and easier to
call correctly.
### `choose`
```python
g.choose(
(state("x").gt(10), big),
(state("y").exists(), has_y),
default=fail,
)
```
Meaning:
```text
if state.x > 10: big
elif state.y exists: has_y
else: fail
```
This is an ordered predicate chain. It should lower through the same condition
construction/connect machinery as `when`, repeated for each clause.
`choose` is intentionally one call. Multiple-call fluent syntax can be built
later on top of it if it proves useful.
## Lowering
These methods may generate core condition nodes, but callers should not need to
know the exact node construction details to choose the right API.
Canonical lowering:
- `branch`
- no new condition nodes
- wires declared outcome strings from one source
- `match`
- ordered equality-check condition chain
- one generated condition per case
- `when`
- one condition node
- `true` and `false` edges
- `choose`
- ordered condition chain
- one generated condition per clause
Trace behavior should document that `match` and `choose` expand to generated
condition nodes.
## Outcome Names
Outcome names are strings at the core wire level, but Python authoring should
eventually avoid handwritten strings when a `NodeSpec` already declares them.
Future improvement:
```python
tool.outcomes.ok
tool.outcomes.error
```
derived from `NodeSpec.outcomes`, not duplicated constants that can drift from
the contract.
This is orthogonal to the control-flow rename, but it belongs in the same
authoring quality bar.
## Migration
`route()` should become deprecated compatibility sugar for one release window,
then be removed.
Recommended behavior during compatibility:
- mark `route()` with `@deprecated` so IDEs surface the replacement path
- `route(PathExpr, cases, ...)`
- warns and forwards to `match(...)`
- `route(Expr, {True: a, False: b}, ...)`
- warns and forwards to `when(...)`
The public docs should prefer only:
- `branch`
- `handle`
- `match`
- `when`
- `choose`
## Shared Outcome Handlers
`handle()` is the reverse-shaped companion to `branch()`: it connects several
source/outcome pairs to one shared target.
```python
g.handle(
(lookup_user, "error"),
(charge_card, "error"),
to=fail,
)
```
Meaning:
```text
lookup_user.error -> fail
charge_card.error -> fail
```
It does not create a join, wait for multiple branches, or inspect state. It is
just outcome-edge sugar for the common "several things fail the same way" case.
## Not In This Pass
- fluent/cursor builder APIs
- operator overloading
- graph-as-node/subgraph support
- JSON draft `match` / `when` / `choose` shapes
Those later layers should depend on this API once it is stable.
## Testing
Tests should prove:
1. `branch` still wires outcome labels only
2. `match` reproduces current value-route behavior
3. `when` reproduces current boolean-route behavior
4. `choose` lowers ordered predicates correctly
5. `route` emits deprecation warnings while preserving old behavior
6. later sugar can delegate to these without needing private builder internals