chore: drop dead context compat, retire plan boxes, fix spec wording
This commit is contained in:
@@ -20,7 +20,7 @@ sugar.
|
|||||||
JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
||||||
|
|
||||||
**Spec:**
|
**Spec:**
|
||||||
[`docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md`](../specs/2026-09-04-structured-runtime-context-design.md)
|
[`docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md`](../../superpowers/specs/2026-09-04-structured-runtime-context-design.md)
|
||||||
|
|
||||||
## Global Constraints
|
## Global Constraints
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
- `FrameContextView.graph` exposes a JSON-compatible `foreach` mapping, all
|
- `FrameContextView.graph` exposes a JSON-compatible `foreach` mapping, all
|
||||||
unique active aliases, and innermost `loop_item`/`loop_index` values.
|
unique active aliases, and innermost `loop_item`/`loop_index` values.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing model and ancestry tests**
|
- [x] **Step 1: Write failing model and ancestry tests**
|
||||||
|
|
||||||
Add focused helpers that construct a `RunState` with explicit frames, then
|
Add focused helpers that construct a `RunState` with explicit frames, then
|
||||||
add tests named:
|
add tests named:
|
||||||
@@ -133,7 +133,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
For the scope-boundary case, give the child root a scheduling parent in the
|
For the scope-boundary case, give the child root a scheduling parent in the
|
||||||
caller's foreach frame and assert that the child context remains `{}`.
|
caller's foreach frame and assert that the child context remains `{}`.
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused tests and confirm the missing API fails**
|
- [x] **Step 2: Run the focused tests and confirm the missing API fails**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Expected: collection or assertions fail because `ForeachContext` and the
|
Expected: collection or assertions fail because `ForeachContext` and the
|
||||||
ancestry-aware helper do not exist.
|
ancestry-aware helper do not exist.
|
||||||
|
|
||||||
- [ ] **Step 3: Add the typed context value and validate frame metadata once**
|
- [x] **Step 3: Add the typed context value and validate frame metadata once**
|
||||||
|
|
||||||
Add `ForeachContext` beside `RuntimeContext` and export it through
|
Add `ForeachContext` beside `RuntimeContext` and export it through
|
||||||
`wf_core.__init__`. Keep `ForeachIterationMetadata` as the typed decoder for
|
`wf_core.__init__`. Keep `ForeachIterationMetadata` as the typed decoder for
|
||||||
@@ -164,7 +164,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Implement fail-closed same-scope ancestry traversal**
|
- [x] **Step 4: Implement fail-closed same-scope ancestry traversal**
|
||||||
|
|
||||||
Walk from the selected frame through `parent_frame_id` while scope ids match.
|
Walk from the selected frame through `parent_frame_id` while scope ids match.
|
||||||
Validate the full chain before returning materialized values:
|
Validate the full chain before returning materialized values:
|
||||||
@@ -226,7 +226,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
graph[LOOP_INDEX_CONTEXT_KEY] = innermost.index
|
graph[LOOP_INDEX_CONTEXT_KEY] = innermost.index
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Add corruption and collision regressions**
|
- [x] **Step 5: Add corruption and collision regressions**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
`run.to_dict()` before the read-only test and assert it remains equal after
|
`run.to_dict()` before the read-only test and assert it remains equal after
|
||||||
deriving context.
|
deriving context.
|
||||||
|
|
||||||
- [ ] **Step 6: Run and commit the focused model slice**
|
- [x] **Step 6: Run and commit the focused model slice**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
- `RuntimeContext.metadata` remains a defensive copy of the selected frame's
|
- `RuntimeContext.metadata` remains a defensive copy of the selected frame's
|
||||||
metadata; `RuntimeContext.foreach` is the canonical typed view.
|
metadata; `RuntimeContext.foreach` is the canonical typed view.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing end-to-end runtime tests**
|
- [x] **Step 1: Write failing end-to-end runtime tests**
|
||||||
|
|
||||||
Extend `test_structured_runtime_context.py` with:
|
Extend `test_structured_runtime_context.py` with:
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Build the binding test with canonical `InputPathBinding` values for
|
Build the binding test with canonical `InputPathBinding` values for
|
||||||
`context.foreach.customers.item` and `context.foreach.orders.item`.
|
`context.foreach.customers.item` and `context.foreach.orders.item`.
|
||||||
|
|
||||||
- [ ] **Step 2: Run the end-to-end tests and observe innermost-only behavior**
|
- [x] **Step 2: Run the end-to-end tests and observe innermost-only behavior**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Expected: the new end-to-end tests fail because runtime consumers still use
|
Expected: the new end-to-end tests fail because runtime consumers still use
|
||||||
innermost frame-only context and handlers do not receive `.foreach`.
|
innermost frame-only context and handlers do not receive `.foreach`.
|
||||||
|
|
||||||
- [ ] **Step 3: Update all graph-visible context call sites together**
|
- [x] **Step 3: Update all graph-visible context call sites together**
|
||||||
|
|
||||||
Replace every old call and verify with search:
|
Replace every old call and verify with search:
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Root workflow output receives `frame_context_view(run, root_frame).graph`
|
Root workflow output receives `frame_context_view(run, root_frame).graph`
|
||||||
rather than an omitted context so standard root facts remain consistent.
|
rather than an omitted context so standard root facts remain consistent.
|
||||||
|
|
||||||
- [ ] **Step 4: Give Python handlers the same typed projection**
|
- [x] **Step 4: Give Python handlers the same typed projection**
|
||||||
|
|
||||||
In `_resolve_node_execution`, materialize the view once and pass its two
|
In `_resolve_node_execution`, materialize the view once and pass its two
|
||||||
projections to their consumers:
|
projections to their consumers:
|
||||||
@@ -374,14 +374,14 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Resolve graph bindings against `context_view.graph`. Do not reconstruct
|
Resolve graph bindings against `context_view.graph`. Do not reconstruct
|
||||||
`ForeachContext` from the graph-visible dictionary.
|
`ForeachContext` from the graph-visible dictionary.
|
||||||
|
|
||||||
- [ ] **Step 5: Update old focused tests to pass `RunState` explicitly**
|
- [x] **Step 5: Update old focused tests to pass `RunState` explicitly**
|
||||||
|
|
||||||
Replace direct frame-only context calls in scheduler/context tests with a run
|
Replace direct frame-only context calls in scheduler/context tests with a run
|
||||||
containing that frame. Keep assertions for existing standard and
|
containing that frame. Keep assertions for existing standard and
|
||||||
compatibility keys; add structured assertions rather than deleting old
|
compatibility keys; add structured assertions rather than deleting old
|
||||||
coverage.
|
coverage.
|
||||||
|
|
||||||
- [ ] **Step 6: Run and commit the runtime integration slice**
|
- [x] **Step 6: Run and commit the runtime integration slice**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
`GraphSourcePath.context(self.id)` because that helper parses dots as path
|
`GraphSourcePath.context(self.id)` because that helper parses dots as path
|
||||||
separators.
|
separators.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing path and serialization tests**
|
- [x] **Step 1: Write failing path and serialization tests**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
assert "index" not in each.model_dump(mode="json")
|
assert "index" not in each.model_dump(mode="json")
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Write failing node and subgraph binding tests**
|
- [x] **Step 2: Write failing node and subgraph binding tests**
|
||||||
|
|
||||||
Use the computed ref directly in both authoring boundaries:
|
Use the computed ref directly in both authoring boundaries:
|
||||||
|
|
||||||
@@ -473,12 +473,12 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Assert both compiled bindings serialize their path as
|
Assert both compiled bindings serialize their path as
|
||||||
`context.foreach.orders.item`.
|
`context.foreach.orders.item`.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement only the two computed properties**
|
- [x] **Step 3: Implement only the two computed properties**
|
||||||
|
|
||||||
Add the properties directly to `ForeachNode`. Do not create `ForeachRef`, a
|
Add the properties directly to `ForeachNode`. Do not create `ForeachRef`, a
|
||||||
node-address type, or field-selection sugar beneath `.item`.
|
node-address type, or field-selection sugar beneath `.item`.
|
||||||
|
|
||||||
- [ ] **Step 4: Run and commit the authoring slice**
|
- [x] **Step 4: Run and commit the authoring slice**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -596,7 +596,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Keep entry schemas inline unless a measured schema-size problem requires
|
Keep entry schemas inline unless a measured schema-size problem requires
|
||||||
`$defs`; the observable field types and required paths are contractual.
|
`$defs`; the observable field types and required paths are contractual.
|
||||||
|
|
||||||
- [ ] **Step 1: Replace innermost-only tests with full-stack expectations**
|
- [x] **Step 1: Replace innermost-only tests with full-stack expectations**
|
||||||
|
|
||||||
Update the existing nested test instead of adding contradictory coverage:
|
Update the existing nested test instead of adding contradictory coverage:
|
||||||
|
|
||||||
@@ -617,7 +617,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Add `test_inner_completion_schema_restores_outer_structured_entry` and assert
|
Add `test_inner_completion_schema_restores_outer_structured_entry` and assert
|
||||||
`after_inner` contains only the outer structured entry.
|
`after_inner` contains only the outer structured entry.
|
||||||
|
|
||||||
- [ ] **Step 2: Run static analysis tests and confirm the old projection fails**
|
- [x] **Step 2: Run static analysis tests and confirm the old projection fails**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -628,7 +628,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Expected: nested assertions fail because `_available_fields` uses only
|
Expected: nested assertions fail because `_available_fields` uses only
|
||||||
`stack[-1]`.
|
`stack[-1]`.
|
||||||
|
|
||||||
- [ ] **Step 3: Build contracts from the whole owner stack**
|
- [x] **Step 3: Build contracts from the whole owner stack**
|
||||||
|
|
||||||
Change `_available_fields` to accept the complete `ForeachOwnerStack`. For
|
Change `_available_fields` to accept the complete `ForeachOwnerStack`. For
|
||||||
every owner id, infer its item schema in the controller's own outer region,
|
every owner id, infer its item schema in the controller's own outer region,
|
||||||
@@ -643,7 +643,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
resolve `over=context.foreach.outer.item.children` without claiming its own
|
resolve `over=context.foreach.outer.item.children` without claiming its own
|
||||||
not-yet-active entry.
|
not-yet-active entry.
|
||||||
|
|
||||||
- [ ] **Step 4: Keep context schema construction reusable and bounded**
|
- [x] **Step 4: Keep context schema construction reusable and bounded**
|
||||||
|
|
||||||
Implement `context_schemas_by_node` by composing the returned contracts, not
|
Implement `context_schemas_by_node` by composing the returned contracts, not
|
||||||
by running a second graph traversal. `context_schema_for_node` is the
|
by running a second graph traversal. `context_schema_for_node` is the
|
||||||
@@ -652,7 +652,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
generated per-node schema; callers treat that absence as invalid, not as
|
generated per-node schema; callers treat that absence as invalid, not as
|
||||||
root context.
|
root context.
|
||||||
|
|
||||||
- [ ] **Step 5: Expose structured paths through authoring inventory**
|
- [x] **Step 5: Expose structured paths through authoring inventory**
|
||||||
|
|
||||||
Add tests showing `context_path_options_for_node(workflow, "inner_body")`
|
Add tests showing `context_path_options_for_node(workflow, "inner_body")`
|
||||||
includes at least:
|
includes at least:
|
||||||
@@ -671,7 +671,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
quoting. Do not hand-concatenate a dotted foreach id; format literal segments
|
quoting. Do not hand-concatenate a dotted foreach id; format literal segments
|
||||||
through `GraphSourcePath`.
|
through `GraphSourcePath`.
|
||||||
|
|
||||||
- [ ] **Step 6: Run and commit the static projection slice**
|
- [x] **Step 6: Run and commit the static projection slice**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -730,7 +730,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
- Ordinary input/state validation remains where it is. The new pass owns the
|
- Ordinary input/state validation remains where it is. The new pass owns the
|
||||||
stronger, program-location-aware meaning of `context.*`.
|
stronger, program-location-aware meaning of `context.*`.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing context-path validation tests**
|
- [x] **Step 1: Write failing context-path validation tests**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -755,7 +755,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
assert "work" in issue.message
|
assert "work" in issue.message
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Cover every model surface that can contain a graph path**
|
- [x] **Step 2: Cover every model surface that can contain a graph path**
|
||||||
|
|
||||||
Parameterize invalid `context.foreach.missing.item` references through:
|
Parameterize invalid `context.foreach.missing.item` references through:
|
||||||
|
|
||||||
@@ -770,7 +770,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
prevents a future path-bearing model from accidentally retaining the current
|
prevents a future path-bearing model from accidentally retaining the current
|
||||||
permissive `allow_context=True` behavior.
|
permissive `allow_context=True` behavior.
|
||||||
|
|
||||||
- [ ] **Step 3: Add one bounded structural path walker**
|
- [x] **Step 3: Add one bounded structural path walker**
|
||||||
|
|
||||||
In `validation/context_paths.py`, use small typed walkers for conditions and
|
In `validation/context_paths.py`, use small typed walkers for conditions and
|
||||||
input expressions:
|
input expressions:
|
||||||
@@ -797,7 +797,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
for every step kind; do not duplicate context validation in each existing
|
for every step kind; do not duplicate context validation in each existing
|
||||||
step validator.
|
step validator.
|
||||||
|
|
||||||
- [ ] **Step 4: Validate context paths against the consuming location**
|
- [x] **Step 4: Validate context paths against the consuming location**
|
||||||
|
|
||||||
For paths whose root is `context`, walk their literal `parts` through the
|
For paths whose root is `context`, walk their literal `parts` through the
|
||||||
consuming node's generated schema. A path is valid only if every segment is
|
consuming node's generated schema. A path is valid only if every segment is
|
||||||
@@ -811,7 +811,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Change `validate_foreach_node` so context-rooted `over` paths reach this pass
|
Change `validate_foreach_node` so context-rooted `over` paths reach this pass
|
||||||
instead of being rejected by the old input/state-only check.
|
instead of being rejected by the old input/state-only check.
|
||||||
|
|
||||||
- [ ] **Step 5: Write failing alias-collision tests**
|
- [x] **Step 5: Write failing alias-collision tests**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -823,7 +823,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
and `loop_index`. The nested failure points to the inner foreach's `as`
|
and `loop_index`. The nested failure points to the inner foreach's `as`
|
||||||
field. Siblings in separate control regions may reuse an alias.
|
field. Siblings in separate control regions may reuse an alias.
|
||||||
|
|
||||||
- [ ] **Step 6: Share control-region analysis during validation**
|
- [x] **Step 6: Share control-region analysis during validation**
|
||||||
|
|
||||||
In `validate_workflow`, run `analyze_control_regions(workflow)` once. Feed the
|
In `validate_workflow`, run `analyze_control_regions(workflow)` once. Feed the
|
||||||
result to context schema construction, translate its issues as today, then
|
result to context schema construction, translate its issues as today, then
|
||||||
@@ -831,7 +831,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
`context_fields_by_node`; add an optional internal `control_regions=` input if
|
`context_fields_by_node`; add an optional internal `control_regions=` input if
|
||||||
necessary while keeping the existing public call form valid.
|
necessary while keeping the existing public call form valid.
|
||||||
|
|
||||||
- [ ] **Step 7: Run and commit the validation slice**
|
- [x] **Step 7: Run and commit the validation slice**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -879,7 +879,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
`foreach_ref.item`, while documenting `RuntimeContext.foreach` as the advanced
|
`foreach_ref.item`, while documenting `RuntimeContext.foreach` as the advanced
|
||||||
handler escape hatch.
|
handler escape hatch.
|
||||||
|
|
||||||
- [ ] **Step 1: Write the interrupt-resume identity regression**
|
- [x] **Step 1: Write the interrupt-resume identity regression**
|
||||||
|
|
||||||
Build `outer.loop -> inner.loop -> ask -> inner -> outer`, interrupt one
|
Build `outer.loop -> inner.loop -> ask -> inner -> outer`, interrupt one
|
||||||
inner item, serialize it with `dump_run_state`, restore it with
|
inner item, serialize it with `dump_run_state`, restore it with
|
||||||
@@ -899,7 +899,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
Do not reuse the original in-memory `RunState`; the loaded value is the
|
Do not reuse the original in-memory `RunState`; the loaded value is the
|
||||||
resume input so reconstruction is genuinely tested.
|
resume input so reconstruction is genuinely tested.
|
||||||
|
|
||||||
- [ ] **Step 2: Write the complete subgraph scope-boundary test**
|
- [x] **Step 2: Write the complete subgraph scope-boundary test**
|
||||||
|
|
||||||
Build a parent foreach and map `each.item` into a saved child subgraph's
|
Build a parent foreach and map `each.item` into a saved child subgraph's
|
||||||
declared input. Give both parent and child a foreach node with id `orders`.
|
declared input. Give both parent and child a foreach node with id `orders`.
|
||||||
@@ -916,7 +916,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
`child_ctx.foreach == {}`. This proves the caller entry was not inherited and
|
`child_ctx.foreach == {}`. This proves the caller entry was not inherited and
|
||||||
the reused static id does not collide.
|
the reused static id does not collide.
|
||||||
|
|
||||||
- [ ] **Step 3: Run the persistence and scope pressure tests**
|
- [x] **Step 3: Run the persistence and scope pressure tests**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -927,7 +927,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
|
|
||||||
Expected: all commands pass.
|
Expected: all commands pass.
|
||||||
|
|
||||||
- [ ] **Step 4: Document the preferred authoring and advanced Python forms**
|
- [x] **Step 4: Document the preferred authoring and advanced Python forms**
|
||||||
|
|
||||||
Add this shape to `docs/wf_authoring_control_flow.md` and the Python skill:
|
Add this shape to `docs/wf_authoring_control_flow.md` and the Python skill:
|
||||||
|
|
||||||
@@ -953,7 +953,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
- child workflows do not inherit caller context and must receive input;
|
- child workflows do not inherit caller context and must receive input;
|
||||||
- `loop_item`, `loop_index`, and aliases are migration conveniences.
|
- `loop_item`, `loop_index`, and aliases are migration conveniences.
|
||||||
|
|
||||||
- [ ] **Step 5: Mark the implementation current and retire the live plan**
|
- [x] **Step 5: Mark the implementation current and retire the live plan**
|
||||||
|
|
||||||
Change the spec status from approved to implemented, replace the roadmap's
|
Change the spec status from approved to implemented, replace the roadmap's
|
||||||
proposed wording with a completed current-runtime statement, and move this
|
proposed wording with a completed current-runtime statement, and move this
|
||||||
@@ -965,7 +965,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
docs skills
|
docs skills
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 6: Run full verification**
|
- [x] **Step 6: Run full verification**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -988,7 +988,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
|
|||||||
remains the only failure, record its exact failing test and verify it also
|
remains the only failure, record its exact failing test and verify it also
|
||||||
fails at the plan's starting commit before treating it as baseline.
|
fails at the plan's starting commit before treating it as baseline.
|
||||||
|
|
||||||
- [ ] **Step 7: Commit the integration and documentation slice**
|
- [x] **Step 7: Commit the integration and documentation slice**
|
||||||
|
|
||||||
Stage exact paths so the user's `docs/AGENTS.md` edit remains untouched:
|
Stage exact paths so the user's `docs/AGENTS.md` edit remains untouched:
|
||||||
|
|
||||||
|
|||||||
@@ -414,7 +414,8 @@ compatibility promise.
|
|||||||
- `loop_item` and `loop_index` select the innermost entry.
|
- `loop_item` and `loop_index` select the innermost entry.
|
||||||
- Unique outer and inner aliases remain available together.
|
- Unique outer and inner aliases remain available together.
|
||||||
- Inner completion restores the outer entry and removes the inner entry.
|
- Inner completion restores the outer entry and removes the inner entry.
|
||||||
- Concurrent item frames receive distinct activation/frame/lineage values.
|
- Concurrent item frames share one visit activation but receive distinct
|
||||||
|
frame/lineage values.
|
||||||
- Interrupt resume recreates the same structured entries from persisted state.
|
- Interrupt resume recreates the same structured entries from persisted state.
|
||||||
- Malformed persisted foreach metadata fails closed.
|
- Malformed persisted foreach metadata fails closed.
|
||||||
|
|
||||||
|
|||||||
@@ -18,29 +18,6 @@ from wf_core.errors import WorkflowExecutionError
|
|||||||
from wf_core.run_state import ExecutionFrame, ForeachContext, RunState
|
from wf_core.run_state import ExecutionFrame, ForeachContext, RunState
|
||||||
|
|
||||||
|
|
||||||
def frame_context_values(frame: ExecutionFrame) -> dict[str, object | None]:
|
|
||||||
context: dict[str, object | None] = {
|
|
||||||
PRIOR_OUTCOME_CONTEXT_KEY: frame.prior_outcome,
|
|
||||||
ACTIVATED_INCOMING_EDGE_CONTEXT_KEY: frame.activated_incoming_edge,
|
|
||||||
SCOPE_ID_CONTEXT_KEY: frame.scope_id,
|
|
||||||
LINEAGE_ID_CONTEXT_KEY: frame.lineage_id,
|
|
||||||
PARENT_LINEAGE_ID_CONTEXT_KEY: frame.parent_lineage_id,
|
|
||||||
}
|
|
||||||
if frame.kind == "foreach_iteration":
|
|
||||||
loop_item = frame.metadata.get("loop_item")
|
|
||||||
loop_index = frame.metadata.get("loop_index")
|
|
||||||
loop_alias = frame.metadata.get("loop_alias")
|
|
||||||
context[LOOP_ITEM_CONTEXT_KEY] = loop_item
|
|
||||||
context[LOOP_INDEX_CONTEXT_KEY] = loop_index
|
|
||||||
if (
|
|
||||||
isinstance(loop_alias, str)
|
|
||||||
and loop_alias
|
|
||||||
and loop_alias not in RESERVED_CONTEXT_KEYS
|
|
||||||
):
|
|
||||||
context[loop_alias] = loop_item
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class FrameContextView:
|
class FrameContextView:
|
||||||
"""Typed handler context and graph values from one ancestry walk."""
|
"""Typed handler context and graph values from one ancestry walk."""
|
||||||
|
|||||||
@@ -199,8 +199,9 @@ def _failing_segment(
|
|||||||
"""Return the first unknown segment plus the keys available there.
|
"""Return the first unknown segment plus the keys available there.
|
||||||
|
|
||||||
Returns ``(None, "")`` when the path walks declared properties (or
|
Returns ``(None, "")`` when the path walks declared properties (or
|
||||||
permissive unconstrained schemas). Diagnostics only; validity follows
|
permissive unconstrained schemas). A bare ``$ref`` fails closed: generated
|
||||||
the same walk as :func:`_path_in_schema`.
|
per-node schemas are inline except for cyclic shapes, which cannot be
|
||||||
|
proven valid statically.
|
||||||
"""
|
"""
|
||||||
if not parts:
|
if not parts:
|
||||||
return None, ""
|
return None, ""
|
||||||
@@ -226,46 +227,6 @@ def _failing_segment(
|
|||||||
return None, ""
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
def _path_in_schema(schema: Mapping[str, Any], parts: tuple[str, ...]) -> bool:
|
|
||||||
"""Return whether literal parts walk declared object properties.
|
|
||||||
|
|
||||||
The whole ``context`` object (no parts) and the ``foreach`` map itself
|
|
||||||
are readable. Unknown segments fail closed, except beneath an
|
|
||||||
unconstrained item schema (``{}`` or an open object with no declared
|
|
||||||
properties): a generic ``array`` collection infers ``{}``, and runtime
|
|
||||||
values may carry fields the static schema cannot see, so such subpaths
|
|
||||||
stay permissive. Closed maps with ``additionalProperties: False`` (like
|
|
||||||
the ``foreach`` map itself) still reject unknown keys.
|
|
||||||
"""
|
|
||||||
if not parts:
|
|
||||||
return True
|
|
||||||
current: Any = schema
|
|
||||||
for part in parts:
|
|
||||||
if not isinstance(current, Mapping):
|
|
||||||
return False
|
|
||||||
# Resolve local $ref if present (defensive; generated schemas are inline).
|
|
||||||
while isinstance(current.get("$ref"), str):
|
|
||||||
# Generated context schemas keep entry schemas inline, so an
|
|
||||||
# unresolvable ref here means the path cannot be proven valid.
|
|
||||||
return False
|
|
||||||
properties = current.get("properties")
|
|
||||||
if not isinstance(properties, Mapping):
|
|
||||||
# No declared properties: unconstrained `{}` or an open object
|
|
||||||
# allows subpaths; scalar or closed schemas do not.
|
|
||||||
if current == {}:
|
|
||||||
return True
|
|
||||||
if (
|
|
||||||
current.get("type") == "object"
|
|
||||||
and current.get("additionalProperties", True) is not False
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
if part not in properties:
|
|
||||||
return False
|
|
||||||
current = properties[part]
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_workflow_output(workflow: Workflow, report: ValidationReport) -> None:
|
def _validate_workflow_output(workflow: Workflow, report: ValidationReport) -> None:
|
||||||
schema = root_context_schema()
|
schema = root_context_schema()
|
||||||
for output_index, binding in enumerate(workflow.output):
|
for output_index, binding in enumerate(workflow.output):
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, StateSchema, Workflow
|
from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, StateSchema, Workflow
|
||||||
from wf_core.analysis.context_scopes import (
|
from wf_core.analysis.context_scopes import (
|
||||||
ContextFieldAvailability,
|
ContextFieldAvailability,
|
||||||
@@ -7,9 +9,10 @@ from wf_core.analysis.context_scopes import (
|
|||||||
context_fields_by_node,
|
context_fields_by_node,
|
||||||
)
|
)
|
||||||
from wf_core.context_contracts import STANDARD_CONTEXT_FIELDS, foreach_context_fields
|
from wf_core.context_contracts import STANDARD_CONTEXT_FIELDS, foreach_context_fields
|
||||||
|
from wf_core.errors import WorkflowExecutionError
|
||||||
from wf_core.models.steps import Step
|
from wf_core.models.steps import Step
|
||||||
from wf_core.run_state import ExecutionFrame
|
from wf_core.run_state import ExecutionFrame, RunState, RunStatus
|
||||||
from wf_core.runtime.ops.frames import frame_context_values
|
from wf_core.runtime.ops.frames import frame_context_view
|
||||||
|
|
||||||
|
|
||||||
def _node(node_id: str) -> NodeUse:
|
def _node(node_id: str) -> NodeUse:
|
||||||
@@ -66,30 +69,48 @@ def _field_map(workflow: Workflow, node_id: str) -> dict[str, ContextFieldAvaila
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_frame_context_values_uses_standard_and_foreach_contract_keys() -> None:
|
def _run_with(*frames: ExecutionFrame) -> RunState:
|
||||||
ordinary = frame_context_values(
|
run = RunState(
|
||||||
ExecutionFrame(
|
workflow_name="demo",
|
||||||
id="root",
|
status=RunStatus.RUNNING,
|
||||||
kind="root",
|
workflow_input={},
|
||||||
node_id="plain",
|
state={},
|
||||||
prior_outcome="ok",
|
|
||||||
activated_incoming_edge="start",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
for frame in frames:
|
||||||
|
run.frames[frame.id] = frame
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
def test_frame_context_view_uses_standard_and_foreach_contract_keys() -> None:
|
||||||
|
root = ExecutionFrame(
|
||||||
|
id="root",
|
||||||
|
kind="root",
|
||||||
|
node_id="plain",
|
||||||
|
prior_outcome="ok",
|
||||||
|
activated_incoming_edge="start",
|
||||||
|
)
|
||||||
|
ordinary = frame_context_view(_run_with(root), root).graph
|
||||||
assert ordinary["prior_outcome"] == "ok"
|
assert ordinary["prior_outcome"] == "ok"
|
||||||
assert ordinary["activated_incoming_edge"] == "start"
|
assert ordinary["activated_incoming_edge"] == "start"
|
||||||
assert ordinary["scope_id"] == "root"
|
assert ordinary["scope_id"] == "root"
|
||||||
assert ordinary["lineage_id"] == "root"
|
assert ordinary["lineage_id"] == "root"
|
||||||
assert ordinary["parent_lineage_id"] is None
|
assert ordinary["parent_lineage_id"] is None
|
||||||
|
assert ordinary["foreach"] == {}
|
||||||
assert "loop_item" not in ordinary
|
assert "loop_item" not in ordinary
|
||||||
|
|
||||||
iteration = ExecutionFrame(
|
iteration = ExecutionFrame(
|
||||||
id="root:each:0",
|
id="root:each:0",
|
||||||
kind="foreach_iteration",
|
kind="foreach_iteration",
|
||||||
node_id="body",
|
node_id="body",
|
||||||
metadata={"loop_item": "a", "loop_index": 0, "loop_alias": "item"},
|
metadata={
|
||||||
|
"foreach_node_id": "each",
|
||||||
|
"activation_id": "act-1",
|
||||||
|
"loop_item": "a",
|
||||||
|
"loop_index": 0,
|
||||||
|
"loop_alias": "item",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context = frame_context_values(iteration)
|
context = frame_context_view(_run_with(iteration), iteration).graph
|
||||||
assert context["loop_item"] == "a"
|
assert context["loop_item"] == "a"
|
||||||
assert context["loop_index"] == 0
|
assert context["loop_index"] == 0
|
||||||
assert context["item"] == "a"
|
assert context["item"] == "a"
|
||||||
@@ -104,13 +125,6 @@ def test_context_contracts_deduplicate_aliases_that_are_standard_loop_keys() ->
|
|||||||
|
|
||||||
|
|
||||||
def test_all_standard_context_names_are_reserved_from_foreach_aliases() -> None:
|
def test_all_standard_context_names_are_reserved_from_foreach_aliases() -> None:
|
||||||
expected_values = {
|
|
||||||
"prior_outcome": "ok",
|
|
||||||
"activated_incoming_edge": "start",
|
|
||||||
"scope_id": "scope",
|
|
||||||
"lineage_id": "lineage",
|
|
||||||
"parent_lineage_id": "parent",
|
|
||||||
}
|
|
||||||
standard_names = {field.name for field in STANDARD_CONTEXT_FIELDS}
|
standard_names = {field.name for field in STANDARD_CONTEXT_FIELDS}
|
||||||
|
|
||||||
for name in standard_names:
|
for name in standard_names:
|
||||||
@@ -118,20 +132,27 @@ def test_all_standard_context_names_are_reserved_from_foreach_aliases() -> None:
|
|||||||
field.name for field in foreach_context_fields(name, {"type": "string"})
|
field.name for field in foreach_context_fields(name, {"type": "string"})
|
||||||
}
|
}
|
||||||
assert name not in foreach_names
|
assert name not in foreach_names
|
||||||
context = frame_context_values(
|
# A reserved alias can never back a runtime entry: the derived view
|
||||||
ExecutionFrame(
|
# fails closed instead of silently shadowing the standard key.
|
||||||
id="child",
|
frame = ExecutionFrame(
|
||||||
kind="foreach_iteration",
|
id="child",
|
||||||
node_id="body",
|
kind="foreach_iteration",
|
||||||
scope_id="scope",
|
node_id="body",
|
||||||
lineage_id="lineage",
|
scope_id="scope",
|
||||||
parent_lineage_id="parent",
|
lineage_id="lineage",
|
||||||
prior_outcome="ok",
|
parent_lineage_id="parent",
|
||||||
activated_incoming_edge="start",
|
prior_outcome="ok",
|
||||||
metadata={"loop_item": "item", "loop_index": 0, "loop_alias": name},
|
activated_incoming_edge="start",
|
||||||
)
|
metadata={
|
||||||
|
"foreach_node_id": "each",
|
||||||
|
"activation_id": "act-1",
|
||||||
|
"loop_item": "item",
|
||||||
|
"loop_index": 0,
|
||||||
|
"loop_alias": name,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert context[name] == expected_values[name]
|
with pytest.raises(WorkflowExecutionError, match="reserved context keys"):
|
||||||
|
frame_context_view(_run_with(frame), frame)
|
||||||
|
|
||||||
|
|
||||||
def test_serial_and_concurrent_foreach_expose_the_same_scoped_context() -> None:
|
def test_serial_and_concurrent_foreach_expose_the_same_scoped_context() -> None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from wf_core.models.schemas import SchemaRef, StateSchema
|
|||||||
from wf_core.models.workflow import Workflow
|
from wf_core.models.workflow import Workflow
|
||||||
from wf_core.run_state import ExecutionFrame, FrameStatus, RunState, RunStatus
|
from wf_core.run_state import ExecutionFrame, FrameStatus, RunState, RunStatus
|
||||||
from wf_core.runtime.ops.flow import advance_frame
|
from wf_core.runtime.ops.flow import advance_frame
|
||||||
from wf_core.runtime.ops.frames import frame_context_values
|
from wf_core.runtime.ops.frames import frame_context_view
|
||||||
from wf_core.runtime.ops.runs import create_run_state
|
from wf_core.runtime.ops.runs import create_run_state
|
||||||
from wf_core.runtime.scheduler import (
|
from wf_core.runtime.scheduler import (
|
||||||
add_frame,
|
add_frame,
|
||||||
@@ -232,19 +232,22 @@ def test_deadlock_error_includes_ready_queue_and_frame_summary() -> None:
|
|||||||
assert "parent:blocked@foreach" in message
|
assert "parent:blocked@foreach" in message
|
||||||
|
|
||||||
|
|
||||||
def test_frame_context_values_exposes_configured_foreach_alias() -> None:
|
def test_frame_context_view_exposes_configured_foreach_alias() -> None:
|
||||||
context = frame_context_values(
|
run = _run()
|
||||||
ExecutionFrame(
|
frame = ExecutionFrame(
|
||||||
id="child",
|
id="child",
|
||||||
kind="foreach_iteration",
|
kind="foreach_iteration",
|
||||||
node_id="body",
|
node_id="body",
|
||||||
metadata={
|
metadata={
|
||||||
"loop_item": {"id": "a"},
|
"foreach_node_id": "each",
|
||||||
"loop_index": 2,
|
"activation_id": "act-1",
|
||||||
"loop_alias": "record",
|
"loop_item": {"id": "a"},
|
||||||
},
|
"loop_index": 2,
|
||||||
)
|
"loop_alias": "record",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
run.frames[frame.id] = frame
|
||||||
|
context = frame_context_view(run, frame).graph
|
||||||
|
|
||||||
assert context["loop_item"] == {"id": "a"}
|
assert context["loop_item"] == {"id": "a"}
|
||||||
assert context["loop_index"] == 2
|
assert context["loop_index"] == 2
|
||||||
|
|||||||
Reference in New Issue
Block a user