docs: design atomic step output bindings

This commit is contained in:
lda
2026-07-23 02:07:07 +07:00 Verified
parent 1f6e3a5925
commit 7809773e7f
@@ -0,0 +1,273 @@
# Atomic Step Output Bindings Design
## Status
Approved for implementation planning on 2026-07-23.
## Problem
Canonical step outputs are ordered `OutputBinding` records that map one
node-local output path into one workflow state path. The focused authoring
surface currently reduces those records to a `dict[str, str]`. That map cannot
represent one local source feeding several state targets, so reading and later
merging a valid canonical list can silently discard fan-out.
The existing focused operation also patches only the binding list. Callers must
separately declare matching state schema paths even when the capability output
schema already describes the source values. This makes a common authoring edit
multi-step and exposes an avoidable intermediate invalid revision.
## Goals
- Replace one capability step's complete ordered output-binding list in one
revision-checked operation.
- Preserve source fan-out and nested local/state paths without lowering through
a dictionary.
- Project missing state schema paths from the capability output schema in the
same atomic mutation.
- Expose the canonical operation through Python, JSON-RPC, MCP, and CLI.
- Keep the existing map operation for real compatibility callers while making
its lossy behavior explicit.
- Reuse the existing schema path and projection helpers instead of adding a
second JSON Schema implementation.
## Non-Goals
- Changing the persisted `OutputBinding` model or runtime output application.
- Supporting literal step outputs. Step outputs read values produced by a node;
literal bindings belong to workflow output projection, not this operation.
- Solving top-level workflow output literals or nested workflow output schema
projection.
- Replacing every legacy output-map reader in this slice.
- Adding a generic input/output binding replacement framework.
- Generating TypeScript RPC contracts.
## Canonical Operation
Add this operation to the public workflow API:
```python
async def set_step_output_bindings(
*,
workspace_id: str,
revision: int,
step_id: str,
bindings: Sequence[OutputBinding],
) -> dict[str, Any]:
"""Replace one capability step's canonical output bindings atomically."""
```
The operation performs these steps in order:
1. Validate the request envelope before entering semantic authoring logic.
2. Check the expected draft revision before reading the step or capability.
3. Require the selected draft step to declare a capability through `use`.
4. Resolve that capability's output schema contract.
5. Validate every local `source` against the capability output schema.
6. Reject duplicate or ancestor/descendant state `target` paths.
7. Project each missing state target from its selected capability output schema
fragment, preserving local `$defs` and `definitions` references.
8. Accept an existing state target only when its schema is exactly equal to the
selected capability output fragment.
9. Replace the step output list and any changed `state_schema` in one patch.
An identical binding list with an identical projected state schema is an exact
no-op: return the current workspace summary without incrementing the revision.
## Binding Semantics
The operation accepts the existing core model unchanged:
```json
[
{"source": "report.title", "target": "state.report.title"},
{"source": "report.title", "target": "state.audit.title"},
{"source": ".", "target": "state.raw_result"}
]
```
- `source` is a rootless `LocalPath`. `.` selects the complete capability
output payload.
- `target` is a non-root `StatePath`, serialized with the `state.` prefix.
- Binding order is preserved exactly.
- Repeating a source is valid and represents fan-out.
- Repeating a target is invalid.
- Ancestor/descendant targets such as `state.report` and
`state.report.title` are invalid because applying both is order-dependent.
- Empty bindings are valid and clear the step output list; they do not remove
previously declared state schema fields.
## Schema Projection
`wf_api.schema_projection` remains the only implementation of JSON Schema path
selection and projection. Output authoring uses
`schema_fragment_at_path(...)`, `schema_path_exists(...)`, and
`project_schema_path_to_schema_path(...)` rather than introducing output-only
schema traversal.
For each binding:
- select `source.parts` from the capability output schema;
- if `target.parts` is absent from the draft state schema, copy the selected
fragment into that target path;
- if the target exists with the exact same schema, leave it unchanged;
- if the target exists with a different schema, reject the operation rather
than guessing JSON Schema compatibility;
- reject unresolved, remote, cyclic, or conflicting schema references through
the existing projection errors.
Projection is monotonic. Replacing or clearing bindings never deletes state
schema declarations because other steps or reducers may still depend on them.
## Compatibility Boundary
Keep `set_step_output_map(...)` and
`workflow.draft_workspaces.set_step_output_map` unchanged for existing callers.
Its `merge=True` mode remains a compatibility adapter and remains inherently
lossy when canonical fan-out already exists.
New code and documentation prefer canonical replacement. The issue describing
lossy compatibility maps remains open until all map readers and writers are
removed or made incapable of rewriting canonical fan-out.
## Transport Design
Add these surfaces:
- Python API: `set_step_output_bindings`;
- JSON-RPC: `workflow.draft_workspaces.set_step_output_bindings`;
- remote RPC client method: `set_step_output_bindings`;
- MCP tool: `wf.workflow.set_step_output_bindings`.
RPC and MCP request models use `list[OutputBinding]` directly. They must preserve
the canonical list order and must not translate through `DraftPathMap`.
Malformed objects fail Pydantic/request-envelope validation before semantic
authoring logic runs.
## CLI Design
Keep the existing command name:
```text
wf draft set-output WORKSPACE --revision N --step STEP ...
```
Preferred canonical forms are:
```bash
wf draft set-output WS \
--revision 4 \
--step analyze \
--map report.title=state.report.title \
--map report.title=state.audit.title
```
and:
```bash
wf draft set-output WS \
--revision 4 \
--step analyze \
--bindings-file output-bindings.json
```
CLI behavior:
- repeatable `--map LOCAL_SOURCE=STATE_TARGET` creates canonical bindings in
flag order;
- `--bindings-file` accepts the exact canonical JSON array and preserves order;
- `--clear` explicitly replaces the list with `[]`;
- no bindings and no `--clear` is an input error;
- `--bindings-file` and `--clear` are mutually exclusive with `--map`;
- canonical replacement does not support `--merge`;
- `--merge --map ...` delegates to the legacy map operation and is documented
as compatibility-only and potentially lossy.
Current bindings remain exportable without another endpoint:
```bash
wf draft inspect WS --include-draft |
jq '.draft.steps.analyze.output' > output-bindings.json
```
## Error Contract
Semantic failures should identify the binding index and canonical path where
possible. Representative errors are:
- `bindings[1].source 'missing' is not declared by capability 'report.analyze'`;
- `bindings[0].target 'state.report' overlaps bindings[1].target 'state.report.title'`;
- `bindings[0].target 'state.report.title' already has an incompatible schema`;
- `bindings[2].source '.' cannot be projected to 'state.raw_result': ...`.
After request-envelope validation, stale revision wins over missing steps,
unknown capabilities, schema lookup, overlap, and projection errors. Every
failure leaves the draft and revision unchanged.
## Testing
### Python Authoring
Add focused tests for:
- ordered replacement with nested local and state paths;
- one local source fanning out to several state targets;
- whole-payload `.` projection;
- missing nested state schema projection;
- exact-equivalent existing target schemas;
- incompatible existing target schemas;
- missing local sources;
- duplicate and ancestor/descendant state targets;
- stable binding order in the stored draft;
- exact no-op behavior;
- explicit clearing without state schema deletion;
- stale revision precedence and no mutation for every failure class;
- compatibility of the existing map-only merge operation.
Compile and execute one representative draft to prove runtime output fan-out
writes the expected values to both state paths.
### Transport And CLI
Add tests proving:
- JSON-RPC, remote client, and MCP preserve canonical output-binding order and
duplicate sources;
- malformed binding objects fail request validation;
- CLI repeated `--map` flags preserve order and fan-out;
- `--bindings-file` round-trips an exported inspected list;
- `--clear` is explicit and an empty accidental invocation fails;
- canonical replacement rejects `--merge` combinations;
- compatibility `--merge --map` remains operational;
- local and remote CLI targets use the same canonical operation.
## Documentation And Issue State
After verification:
- keep the combined input/output map-loss issue open, but update it to state
that both canonical replacements preserve fan-out while map compatibility
operations remain lossy;
- leave workflow-output literals, nested workflow-output projection, focused
step updates, and TypeScript parity open;
- update CLI help, user docs, and agent skills with canonical replacement,
fan-out, clear, compatibility, and export examples;
- add a completed roadmap entry linking to the archived implementation plan;
- archive the implementation plan under
`docs/historical/superpowers/plans/`.
## Success Criteria
- One focused operation replaces a step's complete canonical output-binding
list in one revision.
- Nested paths, source fan-out, and whole-payload output use the existing core
binding model unchanged.
- Every local source is validated against the capability output schema before
mutation.
- Missing state target schemas are projected atomically with the binding list.
- Existing map-only callers retain their current behavior through a clearly
documented compatibility adapter.
- No new persisted binding type, template language, or JSON Schema traversal is
introduced.
- Focused API, runtime, RPC, MCP, CLI, Ruff, formatting, and basedpyright checks
pass.