chore: drop dead context compat, retire plan boxes, fix spec wording

This commit is contained in:
lda
2026-09-05 00:35:00 +07:00 Verified
parent 6e6a4f943d
commit 688fccecc8
6 changed files with 112 additions and 149 deletions
@@ -20,7 +20,7 @@ sugar.
JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
**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
@@ -100,7 +100,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
- `FrameContextView.graph` exposes a JSON-compatible `foreach` mapping, all
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 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
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:
@@ -144,7 +144,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Expected: collection or assertions fail because `ForeachContext` and the
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
`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.
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
```
- [ ] **Step 5: Add corruption and collision regressions**
- [x] **Step 5: Add corruption and collision regressions**
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
deriving context.
- [ ] **Step 6: Run and commit the focused model slice**
- [x] **Step 6: Run and commit the focused model slice**
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
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:
@@ -317,7 +317,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Build the binding test with canonical `InputPathBinding` values for
`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:
@@ -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
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:
@@ -349,7 +349,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Root workflow output receives `frame_context_view(run, root_frame).graph`
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
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
`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
containing that frame. Keep assertions for existing standard and
compatibility keys; add structured assertions rather than deleting old
coverage.
- [ ] **Step 6: Run and commit the runtime integration slice**
- [x] **Step 6: Run and commit the runtime integration slice**
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
separators.
- [ ] **Step 1: Write failing path and serialization tests**
- [x] **Step 1: Write failing path and serialization tests**
Add:
@@ -455,7 +455,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
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:
@@ -473,12 +473,12 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Assert both compiled bindings serialize their path as
`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
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:
@@ -596,7 +596,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Keep entry schemas inline unless a measured schema-size problem requires
`$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:
@@ -617,7 +617,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Add `test_inner_completion_schema_restores_outer_structured_entry` and assert
`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:
@@ -628,7 +628,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Expected: nested assertions fail because `_available_fields` uses only
`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
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
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
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
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")`
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
through `GraphSourcePath`.
- [ ] **Step 6: Run and commit the static projection slice**
- [x] **Step 6: Run and commit the static projection slice**
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
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:
@@ -755,7 +755,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
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:
@@ -770,7 +770,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
prevents a future path-bearing model from accidentally retaining the current
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
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
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
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
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:
@@ -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`
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
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
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:
@@ -879,7 +879,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
`foreach_ref.item`, while documenting `RuntimeContext.foreach` as the advanced
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
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
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
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
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:
@@ -927,7 +927,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
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:
@@ -953,7 +953,7 @@ JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
- child workflows do not inherit caller context and must receive input;
- `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
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
```
- [ ] **Step 6: Run full verification**
- [x] **Step 6: Run full verification**
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
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:
@@ -414,7 +414,8 @@ compatibility promise.
- `loop_item` and `loop_index` select the innermost entry.
- Unique outer and inner aliases remain available together.
- 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.
- Malformed persisted foreach metadata fails closed.
-23
View File
@@ -18,29 +18,6 @@ from wf_core.errors import WorkflowExecutionError
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)
class FrameContextView:
"""Typed handler context and graph values from one ancestry walk."""
+3 -42
View File
@@ -199,8 +199,9 @@ def _failing_segment(
"""Return the first unknown segment plus the keys available there.
Returns ``(None, "")`` when the path walks declared properties (or
permissive unconstrained schemas). Diagnostics only; validity follows
the same walk as :func:`_path_in_schema`.
permissive unconstrained schemas). A bare ``$ref`` fails closed: generated
per-node schemas are inline except for cyclic shapes, which cannot be
proven valid statically.
"""
if not parts:
return None, ""
@@ -226,46 +227,6 @@ def _failing_segment(
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:
schema = root_context_schema()
for output_index, binding in enumerate(workflow.output):
+54 -33
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import pytest
from wf_core import END, Edge, ForeachNode, NodeUse, SchemaRef, StateSchema, Workflow
from wf_core.analysis.context_scopes import (
ContextFieldAvailability,
@@ -7,9 +9,10 @@ from wf_core.analysis.context_scopes import (
context_fields_by_node,
)
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.run_state import ExecutionFrame
from wf_core.runtime.ops.frames import frame_context_values
from wf_core.run_state import ExecutionFrame, RunState, RunStatus
from wf_core.runtime.ops.frames import frame_context_view
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:
ordinary = frame_context_values(
ExecutionFrame(
id="root",
kind="root",
node_id="plain",
prior_outcome="ok",
activated_incoming_edge="start",
)
def _run_with(*frames: ExecutionFrame) -> RunState:
run = RunState(
workflow_name="demo",
status=RunStatus.RUNNING,
workflow_input={},
state={},
)
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["activated_incoming_edge"] == "start"
assert ordinary["scope_id"] == "root"
assert ordinary["lineage_id"] == "root"
assert ordinary["parent_lineage_id"] is None
assert ordinary["foreach"] == {}
assert "loop_item" not in ordinary
iteration = ExecutionFrame(
id="root:each:0",
kind="foreach_iteration",
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_index"] == 0
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:
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}
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"})
}
assert name not in foreach_names
context = frame_context_values(
ExecutionFrame(
id="child",
kind="foreach_iteration",
node_id="body",
scope_id="scope",
lineage_id="lineage",
parent_lineage_id="parent",
prior_outcome="ok",
activated_incoming_edge="start",
metadata={"loop_item": "item", "loop_index": 0, "loop_alias": name},
)
# A reserved alias can never back a runtime entry: the derived view
# fails closed instead of silently shadowing the standard key.
frame = ExecutionFrame(
id="child",
kind="foreach_iteration",
node_id="body",
scope_id="scope",
lineage_id="lineage",
parent_lineage_id="parent",
prior_outcome="ok",
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:
+16 -13
View File
@@ -7,7 +7,7 @@ from wf_core.models.schemas import SchemaRef, StateSchema
from wf_core.models.workflow import Workflow
from wf_core.run_state import ExecutionFrame, FrameStatus, RunState, RunStatus
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.scheduler import (
add_frame,
@@ -232,19 +232,22 @@ def test_deadlock_error_includes_ready_queue_and_frame_summary() -> None:
assert "parent:blocked@foreach" in message
def test_frame_context_values_exposes_configured_foreach_alias() -> None:
context = frame_context_values(
ExecutionFrame(
id="child",
kind="foreach_iteration",
node_id="body",
metadata={
"loop_item": {"id": "a"},
"loop_index": 2,
"loop_alias": "record",
},
)
def test_frame_context_view_exposes_configured_foreach_alias() -> None:
run = _run()
frame = ExecutionFrame(
id="child",
kind="foreach_iteration",
node_id="body",
metadata={
"foreach_node_id": "each",
"activation_id": "act-1",
"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_index"] == 2