docs: design nested local draft bindings

This commit is contained in:
lda
2026-07-22 07:00:53 +07:00 Verified
parent dbb200269b
commit e12a2c57b8
@@ -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.