Accept loop variables in the spelling examples actually use (#469) - #470
Merged
Conversation
The runtime binds each loop variable under two names: item and $item.
The template validator knew only the $-prefixed set:
self.loop_vars = {'$item', '$index', '$is_first', ...}
so {{ item.name }} inside a for_each step -- the form every example in
the catalogue writes -- was reported as "Undefined variable: 'item'".
That is the same false-positive class removed in #448 (slugify), #450
(now), #454 (execution.timestamp) and #459 (pipeline_id): a name the
runtime provides, rejected by validation.
Adding the bare names exposed a second disagreement underneath. The
walker decided a step was a loop with 'for_each' in obj or 'while' in
obj, so a step written with foreach -- an alias the compiler accepts --
was not a loop as far as the validator was concerned, and its loop
variables became "used outside of loop context". The message changed;
the pipeline stayed wrongly rejected. A validator that knows one
spelling and a compiler that accepts another is #465 one layer down.
Both name sets now live in core.template_globals with the rest of the
pipeline language, and data_flow_validator imports the same object
rather than keeping an equal copy.
The gate that makes this safe already existed: in_loop_context. A loop
variable outside a loop is still an error, and a pipeline declaring an
input named item still wins over the loop reading. The prediction in
issue #469 that this trade would introduce a false negative was wrong.
Also fixes a destructive bug in the baseline writer found while
updating it. The file is generated in full, header included, but its
header had been extended by hand when examples/supported/ landed --
so --update silently deleted the paragraph distinguishing "passes
validation" from "is a supported example". The header moves into
BASELINE_HEADER, and a test now asserts the bytes on disk equal what
--update would write, failing loudly on any hand-edit.
Catalogue: 50 -> 52 validating, no listed example regressed.
Blocking suite: 868 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #469.
The bug
The runtime binds each loop variable under two names —
itemand$item. The template validator knew only one of them:So
{{ item.name }}inside afor_eachstep — the form every example in the catalogue writes — was reported asUndefined variable: 'item'.This is the same false-positive class removed in #448 (
slugify), #450 (now), #454 (execution.timestamp) and #459 (pipeline_id): a name the runtime provides, rejected by validation.The second defect underneath it
Adding the bare names did not make the affected examples pass — it changed their error message. The walker decided a step was a loop with:
A step written with
foreach— an alias the compiler accepts — was not a loop as far as the validator was concerned, so its loop variables becameLoop variable used outside of loop context. Same wrongly-rejected pipeline, different complaint. A validator that knows one spelling and a compiler that accepts another is #465 one layer down.LOOP_STEP_KEYSnow coversfor_each,foreach,while,create_parallel_queue,action_loop.Correcting the issue
Issue #469 as I filed it predicted that accepting bare names would trade a false positive for a false negative. That was wrong. The
in_loop_contextgate already existed, so{{ item }}outside a loop is still an error, and a pipeline that declares its own input nameditemstill wins over the loop reading. Both are pinned by tests.One declaration
Both name sets move into
core.template_globalsalongside the rest of the pipeline language.data_flow_validatorimports the same object rather than keeping an equal copy — the test asserts identity, not equality, because an equal copy is exactly what drifted into #469.Baseline writer bug, found while updating the baseline
--updateregenerates the file in full, header included. The header had been extended by hand whenexamples/supported/landed, so my first--updatesilently deleted the paragraph distinguishing "passes validation" from "is a supported example" — an 8-line diff for two added entries.Hand-fixing the file would have restored the text and left the trap armed. The header moves into
BASELINE_HEADER, and a new test asserts the bytes on disk equal whatwrite_baseline()would produce, so any hand-edit to the generated file is a red test rather than a deletion later.Verification
Three mutations, all caught:
$-only name setfor_each/whilecount as loopsE9,F63,F7,F82,F821,F823,F601,F811)examples/web_research_pipeline.yamlnow fails only oncurrent_date, a name the runtime genuinely does not provide — a real defect in that example, correctly reported.🤖 Generated with Claude Code