docs: design nested local draft bindings
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
# Nested Local Draft Bindings Design
|
||||
|
||||
## Goal
|
||||
|
||||
Bring focused draft authoring into parity with the canonical runtime model for
|
||||
nested node-local paths. Agents and operators should be able to bind structured
|
||||
capability inputs and outputs without raw JSON Patch, while the implementation
|
||||
reuses canonical path types and one shared JSON Schema projection module.
|
||||
|
||||
## Problem
|
||||
|
||||
`LocalPath`, workflow validation, runtime input assembly, runtime output reads,
|
||||
and `WorkflowBuilder` already support paths such as `report.title`. Focused
|
||||
draft authoring is narrower:
|
||||
|
||||
- `wf draft bind --from input.title --to local.report.title` rejects the target
|
||||
because `bind_draft` requires one local path segment;
|
||||
- the inverse output direction rejects nested local sources for the same
|
||||
reason;
|
||||
- capability-step insertion persists nested input targets but silently skips
|
||||
schema projection whenever the local path has more than one segment;
|
||||
- CLI help describes step input targets as bare fields rather than canonical
|
||||
node-local paths.
|
||||
|
||||
The limitation is not in the workflow model. It is duplicated path and schema
|
||||
handling in the focused authoring layer.
|
||||
|
||||
## Scope
|
||||
|
||||
This slice supports nested node-local paths for:
|
||||
|
||||
- workflow input or state to capability input:
|
||||
`input.title -> local.report.title`;
|
||||
- capability output to workflow state:
|
||||
`local.report.markdown -> state.report.markdown`;
|
||||
- capability output to public workflow output, using the existing durable-state
|
||||
lowering:
|
||||
`local.report.markdown -> output.report.markdown` becomes a node output
|
||||
binding to `state.report.markdown` plus a workflow output binding from that
|
||||
state path;
|
||||
- capability-step input maps such as
|
||||
`--map input.title=report.title`, where the node-local root remains implied by
|
||||
the existing command interface.
|
||||
|
||||
The slice also centralizes repeated authoring-time JSON Schema path operations
|
||||
and updates CLI/agent-facing path documentation.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
This slice does not:
|
||||
|
||||
- add literal node-input or workflow-output bindings;
|
||||
- replace dictionary maps with a fan-out-safe binding-list interface;
|
||||
- add the atomic structured-input assembly helper tracked separately in
|
||||
`ISSUES.md`;
|
||||
- add step metadata or focused update-step operations;
|
||||
- add TypeScript JSON-RPC operations or code generation;
|
||||
- change route, revision, deployment, or runtime semantics;
|
||||
- implement a general JSON Schema resolver or support remote references;
|
||||
- change existing CLI command names or request fields.
|
||||
|
||||
## Canonical Path Interfaces
|
||||
|
||||
No new path model is introduced.
|
||||
|
||||
- `GraphSourcePath` parses workflow-readable `input.*`, `state.*`, and
|
||||
`context.*` paths.
|
||||
- `LocalPath` parses rootless node-local paths such as `report.title` and the
|
||||
whole-payload marker `.`.
|
||||
- `wf draft bind` continues using explicit endpoint roots because both sides
|
||||
are endpoints: `--from input.title --to local.report.title`.
|
||||
- capability-add and set-input maps continue using rootless local targets
|
||||
because their interface already implies the node-local side:
|
||||
`--map input.title=report.title`.
|
||||
|
||||
The CLI parser must validate rootless map targets with `LocalPath.parse` instead
|
||||
of checking only whether they start with `local.`. Existing single-segment map
|
||||
syntax remains valid. Rooted `local.*` map targets remain rejected with an exact
|
||||
repair example because changing that syntax is a separate migration.
|
||||
|
||||
Transport request fields remain strings. Their endpoint role is determined by
|
||||
the existing command/method interface, and the semantic authoring module parses
|
||||
them through the canonical path classes. Do not create duplicate Pydantic path
|
||||
schemas or endpoint unions in RPC models for this slice.
|
||||
|
||||
## Shared JSON Schema Projection Module
|
||||
|
||||
Deepen `wf_api.schema_projection` so authoring code does not perform schema
|
||||
traversal itself.
|
||||
|
||||
### Public Operations
|
||||
|
||||
The module exposes:
|
||||
|
||||
```python
|
||||
def schema_path_exists(
|
||||
schema: Mapping[str, Any],
|
||||
parts: Sequence[str],
|
||||
) -> bool: ...
|
||||
|
||||
|
||||
def project_schema_path_to_schema_path(
|
||||
*,
|
||||
target_schema: JsonObject,
|
||||
source_schema: JsonObject,
|
||||
source_parts: tuple[str, ...],
|
||||
target_parts: tuple[str, ...],
|
||||
allow_existing_equivalent: bool = False,
|
||||
) -> JsonObject: ...
|
||||
```
|
||||
|
||||
`project_property_to_schema_path` remains as a compatibility-preserving
|
||||
root-property wrapper and delegates to the new operation with
|
||||
`source_parts=(source_field,)`. Existing callers and error wording remain
|
||||
stable where practical.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
The module:
|
||||
|
||||
1. validates source and target documents with `jsonschema`;
|
||||
2. traverses nested object `properties` using path segments supplied by
|
||||
canonical path types;
|
||||
3. resolves bounded local references used by repository-generated schemas,
|
||||
including `#/$defs/...` and `#/definitions/...`;
|
||||
4. copies the selected source subschema into the requested target object path;
|
||||
5. carries `$defs` and `definitions` blocks needed by copied local references;
|
||||
6. validates the projected result;
|
||||
7. reports missing source paths, non-object intermediate paths, unsupported
|
||||
references, and conflicting target paths precisely.
|
||||
|
||||
This is authoring-time schema projection, not schema inference. The module does
|
||||
not create new Pydantic models mirroring capability schemas and does not
|
||||
evaluate arbitrary combinators or remote references. Capability schemas remain
|
||||
the documents emitted by `NodeSpec` contracts or Pydantic
|
||||
`model_json_schema()`.
|
||||
|
||||
The duplicate `_schema_path_exists` implementations in `wf_api.drafts` and
|
||||
`wf_api.draft_authoring` move into this module. Both callers import the shared
|
||||
operation so target-path behavior has one implementation.
|
||||
|
||||
## Authoring Data Flow
|
||||
|
||||
### Focused Bind: Workflow Data To Nested Capability Input
|
||||
|
||||
For `input.title -> local.report.title`:
|
||||
|
||||
1. revision preflight runs before semantic work;
|
||||
2. `GraphSourcePath` parses `input.title`;
|
||||
3. `LocalPath` parses rootless `report.title` from the explicit `local.*`
|
||||
endpoint;
|
||||
4. the capability input schema is selected from its explicit contract or
|
||||
Pydantic model;
|
||||
5. `project_schema_path_to_schema_path` copies the capability schema at
|
||||
`report.title` into the workflow input schema at `title` when absent;
|
||||
6. `input_bindings_payload` serializes the canonical nested local target;
|
||||
7. the existing revision-checked patch operation persists both changes.
|
||||
|
||||
Existing workflow source schema paths are reused without projection, preserving
|
||||
current idempotent behavior.
|
||||
|
||||
### Focused Bind: Nested Capability Output To State Or Output
|
||||
|
||||
For `local.report.markdown -> state.report.markdown`, the capability output
|
||||
schema at `report.markdown` is copied to the requested state path and the step
|
||||
output binding stores the complete nested `LocalPath`.
|
||||
|
||||
For `local.report.markdown -> output.report.markdown`, the existing output
|
||||
lowering remains unchanged except that the nested capability subschema is used.
|
||||
The operation projects that subschema into both state and public output schemas,
|
||||
writes the node output into state, and projects workflow output from the same
|
||||
state path.
|
||||
|
||||
### Capability-Step Insertion
|
||||
|
||||
For `input.title=report.title`:
|
||||
|
||||
1. `GraphSourcePath.parse` validates the graph source;
|
||||
2. `LocalPath.parse` validates the rootless local target;
|
||||
3. the complete local parts tuple selects the nested capability input
|
||||
subschema;
|
||||
4. that subschema is projected into the workflow input/state source path when
|
||||
the workflow schema does not already declare it;
|
||||
5. the nested input binding is persisted in the same atomic patch as the new
|
||||
step and routes.
|
||||
|
||||
Remove the current `len(local_parts) != 1` skip. A valid nested local path must
|
||||
never silently disable schema projection.
|
||||
|
||||
## Errors And Compatibility
|
||||
|
||||
Current single-field bindings keep their response shapes and semantics.
|
||||
|
||||
For current revisions:
|
||||
|
||||
- an absent nested capability schema path raises a precise `ValueError` naming
|
||||
the complete path;
|
||||
- traversing through a scalar or otherwise non-object schema raises a precise
|
||||
`ValueError` naming the blocking prefix;
|
||||
- an unsupported or unresolved reference raises a precise `ValueError` rather
|
||||
than silently skipping projection;
|
||||
- an existing workflow input/state source path is reused unchanged, preserving
|
||||
current bind behavior;
|
||||
- output/state projection continues accepting exact equivalent target schemas
|
||||
and rejecting incompatible existing targets;
|
||||
- no patch is persisted when projection fails.
|
||||
|
||||
Revision precedence from the preceding slice remains intact: after intrinsic
|
||||
request validation, stale requests return `revision_conflict` before path or
|
||||
schema errors derived from current workspace/catalog state.
|
||||
|
||||
Transport fields and response envelopes do not change. RPC, remote-client, and
|
||||
CLI layers inherit behavior through existing delegation. Documentation-only
|
||||
description changes may clarify nested examples but must not introduce parallel
|
||||
request schemas.
|
||||
|
||||
## Testing
|
||||
|
||||
### Schema Projection
|
||||
|
||||
Add focused unit tests for:
|
||||
|
||||
- inline nested source properties;
|
||||
- nested source properties behind Pydantic-style `#/$defs` references;
|
||||
- legacy `#/definitions` local references;
|
||||
- copied definition blocks remaining resolvable;
|
||||
- missing source paths;
|
||||
- non-object intermediate source paths;
|
||||
- conflicting and equivalent target paths;
|
||||
- centralized `schema_path_exists` behavior.
|
||||
|
||||
### Python Authoring
|
||||
|
||||
Add API tests for:
|
||||
|
||||
- input and state binding to nested local targets;
|
||||
- nested local output binding to nested state;
|
||||
- nested local output binding to public output through state;
|
||||
- capability-step insertion with nested input target and missing workflow schema
|
||||
projection;
|
||||
- current-revision invalid nested capability paths producing clear errors and
|
||||
no mutation;
|
||||
- stale revision winning over nested-path semantic errors;
|
||||
- existing single-segment and idempotent cases remaining unchanged.
|
||||
|
||||
### Transport And CLI
|
||||
|
||||
Add delegation/regression tests proving:
|
||||
|
||||
- JSON-RPC and the remote client preserve nested path strings unchanged;
|
||||
- `wf draft bind` accepts explicit rooted nested local endpoints;
|
||||
- capability-add and set-input map parsing accept rootless nested local paths;
|
||||
- rooted `local.*` map targets remain rejected with a repair example;
|
||||
- help and agent instructions distinguish explicit bind endpoints from implied
|
||||
local map targets.
|
||||
|
||||
Compile or validate the resulting draft to prove the stored canonical bindings
|
||||
are accepted by the existing workflow model. Runtime nested mapping behavior is
|
||||
already covered in `tests/core/test_nested_mappings.py` and should not be
|
||||
reimplemented in this slice.
|
||||
|
||||
## Documentation And Issue State
|
||||
|
||||
After verification:
|
||||
|
||||
- check the nested `wf draft bind` limitation;
|
||||
- check the capability-step nested projection limitation;
|
||||
- check the CLI help/agent instruction limitation;
|
||||
- leave atomic structured-input assembly, literal bindings, fan-out maps,
|
||||
nested workflow-output source projection, step metadata, and TypeScript parity
|
||||
open;
|
||||
- add a completed roadmap entry linking to the archived implementation plan;
|
||||
- archive the implementation plan under
|
||||
`docs/historical/superpowers/plans/`.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Focused bind supports nested `LocalPath` values in both input and output
|
||||
directions.
|
||||
- Capability-step insertion projects schemas for nested local input targets
|
||||
instead of silently skipping them.
|
||||
- Existing single-segment bind and map behavior remains compatible.
|
||||
- No new transport request schema or duplicate path type is introduced.
|
||||
- Common schema path lookup, local-reference handling, existence checks, and
|
||||
projection live in `wf_api.schema_projection`.
|
||||
- Resulting drafts compile or validate through the existing canonical workflow
|
||||
model.
|
||||
- Focused API/RPC/CLI tests, Ruff, formatting, basedpyright, and relevant core
|
||||
regressions pass.
|
||||
Reference in New Issue
Block a user