Every binding below was observed by executing a pipeline (filesystem tool only, no models) and reading what reached the file.
A construct renders names it has no concept of, and renders them as a plausible-looking value rather than failing:
| construct |
leaked name |
renders as |
why it is wrong |
for_each body |
iteration |
None |
a for_each has no iteration counter |
while body |
item |
None |
a while loop walks no collection |
while body |
length, remaining, has_next, has_prev |
0, 0, False, True |
collection values for a construct with no collection |
for_each iterable |
index, is_first, is_last, position, length, ... |
0, True, ... |
the iterable resolves before the loop exists; these come from a zeroed context that is not the loop's |
create_parallel_queue.on |
index, is_first, is_last, queue_size, parallel_queue_id, parent_task |
0, True, ... |
same: on generates the queue, so nothing per-item exists yet |
{{ item }} in a for_each: iterable does error, so the boundary is enforced for exactly one name.
The mechanism for the body cases: ControlSystem._render_task_templates registers every key of metadata["loop_context"] (core/control_system.py:263-267), and that value is LoopContext.get_debug_info() (core/loop_context.py:243) — a dict built for debugging, carrying item_type, nesting_depth, is_current_first and the rest alongside the real bindings. A debug dump became the public template surface by accident.
A narrower list exists 40 lines later (item, index, is_first, is_last, $item, $index) but runs after the wholesale registration and cannot take anything back.
This matters for validation because it makes "what does this construct bind?" unanswerable from the runtime alone: the honest answer is "these six, plus everything in a debug dict". core/loop_contracts.py therefore declares the meaningful bindings and withholds the leaked ones, so validation rejects {{ iteration }} in a for_each. That is a deliberate choice to be stricter than the runtime, and it is the wrong way round — the runtime should stop offering them.
Suggested order:
- Give
LoopContext an explicit to_template_dict()-style public surface, separate from get_debug_info().
- Register only that surface in
control_system.py.
- Make the two for_each paths (
control_system and the ForEachTask expansion) populate the same surface — they currently differ.
- Then
loop_contracts describes the runtime instead of correcting it.
Every binding below was observed by executing a pipeline (filesystem tool only, no models) and reading what reached the file.
A construct renders names it has no concept of, and renders them as a plausible-looking value rather than failing:
for_eachbodyiterationNonewhilebodyitemNonewhilebodylength,remaining,has_next,has_prev0,0,False,Truefor_eachiterableindex,is_first,is_last,position,length, ...0,True, ...create_parallel_queue.onindex,is_first,is_last,queue_size,parallel_queue_id,parent_task0,True, ...ongenerates the queue, so nothing per-item exists yet{{ item }}in afor_each:iterable does error, so the boundary is enforced for exactly one name.The mechanism for the body cases:
ControlSystem._render_task_templatesregisters every key ofmetadata["loop_context"](core/control_system.py:263-267), and that value isLoopContext.get_debug_info()(core/loop_context.py:243) — a dict built for debugging, carryingitem_type,nesting_depth,is_current_firstand the rest alongside the real bindings. A debug dump became the public template surface by accident.A narrower list exists 40 lines later (
item,index,is_first,is_last,$item,$index) but runs after the wholesale registration and cannot take anything back.This matters for validation because it makes "what does this construct bind?" unanswerable from the runtime alone: the honest answer is "these six, plus everything in a debug dict".
core/loop_contracts.pytherefore declares the meaningful bindings and withholds the leaked ones, so validation rejects{{ iteration }}in afor_each. That is a deliberate choice to be stricter than the runtime, and it is the wrong way round — the runtime should stop offering them.Suggested order:
LoopContextan explicitto_template_dict()-style public surface, separate fromget_debug_info().control_system.py.control_systemand the ForEachTask expansion) populate the same surface — they currently differ.loop_contractsdescribes the runtime instead of correcting it.