Files
lda-wf/docs/historical/superpowers/plans/2026-05-17-nested-node-local-mappings.md
T

326 lines
8.6 KiB
Markdown

# Nested Node-Local Mappings Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let workflow maps address nested node-local input/output paths while preserving explicit state-mediated data flow and preparing state writes for future reducer libraries.
**Architecture:** Keep graph-facing paths unchanged. Add a small node-local path helper layer, validate non-overlapping write targets, construct nested node inputs from `in_map`, resolve nested node outputs from `out_map`, and refactor state writes into a prepared patch commit. Extract built-in merge-rule application behind a focused reducer-like module so future named reducer libraries can replace the dispatch seam without changing patch semantics.
**Tech Stack:** Python, Pydantic, pytest, existing `wf_core` path/state runtime.
---
## File Structure
- Modify `src/wf_core/paths.py`
- keep graph-path helpers
- add reusable path-overlap utility if it belongs at the generic path layer
- Create `src/wf_core/local_paths.py`
- node-local dotted path parsing
- nested get/set for node-local payloads
- overlap checks for write targets
- Create `src/wf_core/runtime/ops/merges.py`
- built-in exact-path merge-rule implementations
- future seam for named reducer registry
- Modify `src/wf_core/runtime/ops/state.py`
- replace per-write mutation loop with prepared patch commit
- delegate merge-rule application to `merges.py`
- Modify `src/wf_core/runtime/ops/nodes.py`
- build nested node inputs from `in_map`
- resolve nested node outputs into state patch writes
- Modify `src/wf_core/validation/steps.py`
- validate node-local top-level roots
- reject overlapping write targets
- Add/modify tests under `tests/core/`
- nested `in_map`
- nested `out_map`
- whole-object mapping still works
- overlapping write targets rejected
- missing nested output path fails
- merge dispatch remains behaviorally unchanged
---
### Task 1: Pin Node-Local Path Behavior
**Files:**
- Modify: `tests/core/test_validation.py`
- Modify: `tests/core/test_runtime.py`
- [ ] **Step 1: Add failing validation tests**
Cover:
```python
def test_validation_allows_nested_node_local_paths() -> None:
...
def test_validation_rejects_overlapping_node_input_destinations() -> None:
...
def test_validation_rejects_overlapping_state_write_destinations() -> None:
...
```
Expected rules:
- `state.person.name -> user.name` is valid when `user` exists in the node input schema
- `user -> state.person` and `user.name -> state.person.name` in one `out_map` is invalid because destination state paths overlap
- `state.person -> user` plus `state.person.name -> user.name` in one `in_map` is invalid because destination node-local paths overlap
- [ ] **Step 2: Add failing runtime tests**
Cover:
```python
def test_runtime_builds_nested_node_input_from_in_map() -> None:
...
def test_runtime_reads_nested_node_output_from_out_map() -> None:
...
def test_runtime_missing_nested_node_output_path_fails() -> None:
...
```
- [ ] **Step 3: Run focused tests and confirm failure**
Run:
```bash
uv run --with pytest pytest tests/core/test_validation.py tests/core/test_runtime.py -q
```
Expected: FAIL because node-local map sides are top-level-only today.
### Task 2: Add Node-Local Path Helpers
**Files:**
- Create: `src/wf_core/local_paths.py`
- Modify: `src/wf_core/validation/steps.py`
- [ ] **Step 1: Add minimal helper API**
Implement:
```python
def split_local_path(path: str) -> list[str]: ...
def get_local_value(payload: Mapping[str, Any], path: str) -> Any: ...
def set_local_value(payload: dict[str, Any], path: str, value: Any) -> None: ...
def paths_overlap(left: str, right: str) -> bool: ...
def has_overlapping_paths(paths: Iterable[str]) -> bool: ...
```
Rules:
- dotted local paths only
- no empty segments
- overlap means same path or ancestor/descendant path
- [ ] **Step 2: Update validation**
Use node-local path roots for schema checks:
```python
input_root = split_local_path(destination_path)[0]
output_root = split_local_path(source_path)[0]
```
Reject:
- overlapping `in_map` destination local paths
- overlapping `out_map` destination state paths
- [ ] **Step 3: Run focused validation tests**
Run:
```bash
uv run --with pytest pytest tests/core/test_validation.py -q
```
Expected: PASS for validation-specific cases.
### Task 3: Execute Nested Local Mappings
**Files:**
- Modify: `src/wf_core/runtime/ops/nodes.py`
- Modify: `src/wf_core/runtime/ops/state.py`
- [ ] **Step 1: Build nested node inputs**
Replace flat input construction with:
```python
resolved_input: dict[str, Any] = {}
for source_path, destination_path in node.in_map.items():
value = safe_resolve_path(...)
set_local_value(resolved_input, destination_path, value)
```
- [ ] **Step 2: Resolve nested node outputs**
When preparing mapped output writes, use `get_local_value()` for each `out_map`
source path instead of indexing only top-level output keys.
- [ ] **Step 3: Preserve missing-path failures**
Raise `WorkflowExecutionError` when a mapped nested output path is missing.
- [ ] **Step 4: Run focused runtime tests**
Run:
```bash
uv run --with pytest pytest tests/core/test_runtime.py -q
```
Expected: PASS for nested mapping behavior.
### Task 4: Introduce Prepared Patch Commits and Extract Merge Dispatch
**Files:**
- Create: `src/wf_core/runtime/ops/merges.py`
- Modify: `src/wf_core/runtime/ops/state.py`
- Modify: `tests/core/test_state_ops.py`
- [ ] **Step 1: Add failing patch-level tests**
Cover:
```python
def test_state_patch_rejects_overlapping_destinations_before_mutation() -> None:
...
def test_builtin_merge_rules_preserve_existing_behavior() -> None:
...
```
- [ ] **Step 2: Extract built-in merge implementations**
Move the current strategy body out of `write_state_value()` into focused helpers:
```python
def apply_builtin_merge(
*,
strategy: str,
current_value: Any,
incoming_value: Any,
destination_path: str,
) -> Any: ...
```
Keep current semantics:
- `replace`
- `append`
- shallow `merge_object`
Add a docstring that this is the future seam for source-owned named reducers,
not custom reducer support yet.
- [ ] **Step 3: Prepare full write sets before mutation**
Refactor output mapping so it:
1. resolves all mapped output values
2. validates destination overlap
3. prepares the patch
4. applies merge behavior
No state changes should occur before all mapped output paths are known-good.
- [ ] **Step 4: Run state/runtime tests**
Run:
```bash
uv run --with pytest pytest tests/core/test_state_ops.py tests/core/test_runtime.py -q
```
Expected: PASS.
### Task 5: Keep Authoring and Docs Aligned
**Files:**
- Modify: `src/wf_authoring/builder/mapping.py`
- Modify: `docs/core_state_mapping_and_merge.md` if implementation details differ
- Modify: `docs/scratchpad.md` only if wording drift appears
- Modify/Add: authoring tests as needed
- [ ] **Step 1: Confirm authoring auto-maps remain top-level**
Automatic maps should stay conservative unless there is an explicit reason to
infer nested paths. The new feature is for explicit maps first.
- [ ] **Step 2: Add one authoring regression**
Prove that a builder can compile a workflow using explicit nested local map
paths without extra helper nodes.
- [ ] **Step 3: Update docs only for implementation drift**
The design doc already states the target behavior. Keep docs in sync with final
names and module boundaries, but do not broaden scope into nested state
declarations yet.
### Task 6: Verify the Whole Project
**Files:**
- No additional files.
- [ ] **Step 1: Run focused suites**
```bash
uv run --with pytest pytest tests/core tests/authoring -q
```
- [ ] **Step 2: Run the full suite**
```bash
uv run --with pytest pytest -q
```
- [ ] **Step 3: Run type checking**
```bash
uv run basedpyright --level error
```
Expected:
- tests pass
- any remaining basedpyright failures are called out explicitly if they come
from existing generated/build/doc-fixture noise rather than this work
---
## Deliberate Non-Goals
- nested declared state merge metadata
- reducer capability registry
- deep merge behavior
- native subgraphs
- parallel foreach
- automatic inference of nested maps from schemas
## Follow-On Plans
After this lands:
1. nested declared state paths with exact-path merge lookup
2. reducer capability model / registry seam
3. native subgraph design on the same map + patch boundary
4. async-only parallel foreach using patch combination rules