DRIFT · confirmed_bug
Nested loops/conditionals, recursive @<path> includes, include-cycle/escape boundary rejection, custom --brace-count and --variable-delimiters modes, whitespace control, Unicode variable values/content, and optional-field defaulting all behaved correctly against the documented contract. Two cases (same root cause) showed validation falsely flagging the standard Jinja `loop` context object (loop.last, loop.index, etc.) as an undeclared referenced token inside for-loops: a warning by default, but a hard validation/render failure under --strict.
| Fuzz run description | Iterations | Pass | Result |
|---|
| Jinja rendering probe across example templates and synthetic .j2 fixtures covering nested loops/conditionals, @<path> includes (cycle/escape/recursive), custom brace-count and variable-delimiter modes, whitespace control, Unicode content, optional/missing fields, and the loop.* Jinja builtin under default and --strict validation. | 18 | 16/18 | FAIL |
|---|
Findings
FUZZ-TP-01
- Minimal template
---
required_variables:
- items
---
{% for x in items %}{{ loop.last }}{% endfor %}
- Input
{"items": ["a", "b", "c"]}- Expected
- docs/requirements.md FR-1035 requires full Jinja for-loop support; the Jinja `loop` context object (loop.last, loop.first, loop.index, loop.index0, loop.length, loop.revindex) is a language-level construct auto-injected by the for-loop, not a caller-supplied or frontmatter-declared token, so it should never be classified as an undeclared referenced token under FR-2a/FR-2c.
- Observed
- Default mode: render succeeds but emits {"code":"ERR_VAL_UNDECLARED_TOKEN","severity":"warning","message":"undeclared referenced token: loop.last"} (also reproduced for loop.index). Under --strict: same diagnostic promoted to severity error, process exits 2, no output rendered -- reproduced 3/3, and again independently with custom --variable-delimiters, confirming the bug is in identifier discovery, not delimiter-mode specific.
- Requirement / ADR
- docs/requirements.md FR-1035 (Jinja for-loop field access) and FR-2c (built-in render-context variable list, from which `loop` is absent because it is not a render-context variable, it is a Jinja loop-scope construct). FR-2a governs undeclared referenced tokens, which by its own framing means caller-facing variable names, not engine-internal loop state.
- Requirement / ADR follow-up
- Update FR-2a to explicitly state that the Jinja `loop` object and its standard attributes are excluded from undeclared-token discovery in every for-loop scope, mirroring how validation.rs::parse_for_loop_scope already excludes the loop's own bound iteration variable(s). No new ADR needed -- this is a bug-fix-sized correction to an existing requirement's implementation.
- Root cause
- In crates/sc-composer/src/validation.rs, parse_for_loop_scope (~line 904) only adds the for-loop's bound iteration variable name(s) to LoopScope.bound_names; it never adds the implicit `loop` identifier Jinja injects into every for-loop body. collect_identifiers (~line 930) checks bound_names and a fixed KEYWORDS list (~line 935) for exclusion, and `loop` appears in neither, so loop.last/loop.index get treated as ordinary undeclared caller tokens.
- Recommended fix
- In parse_for_loop_scope, unconditionally push "loop" into the returned LoopScope.bound_names set alongside the iteration binding name(s), so collect_identifiers treats any loop.<attr> reference inside that for-loop's lexical scope as bound/declared rather than an undeclared caller token. Add a regression test asserting discover_tokens_with_brace_count returns an empty set for a template body using loop.last/loop.index, plus a CLI-level test asserting render --strict succeeds for the same template.
XHTML panel validation (xmllint --noout): FAILED (invalid XML: unescaped literal '<', '@<path>' include syntax and '<<' delimiter examples in test-input descriptions)