FUZZ-001
- Minimal template / frontmatter
@<{{ "a.md" if x else "b.md" if y else "c.md" }}>- Input
a.md="A\n", b.md="B\n", c.md="C\n" (any values for x/y; --var x=true --var y=false)- Expected
- A 3-way chained Jinja ternary inside @<{{ ... }}> is either resolved correctly to one of the three candidates, or rejected cleanly as IncludeDirective::Dynamic (ERR_INCLUDE_DYNAMIC_UNRESOLVED), matching how the single-level "a" if x else "b" form is already handled.
- Observed
- parse_conditional_expression() (directive.rs) splits only once on " if " and once on " else ". For the else-branch text '"b.md" if y else "c.md"', quoted_literal() sees a string that starts and ends with a double-quote byte and accepts the ENTIRE span -- including the embedded ' if y else ' -- as one opaque literal. The parser therefore builds a Conditional{condition:"x", candidates:["a.md", "b.md\" if y else \"c.md"]} instead of failing, and expansion tries to open the literal garbage path 'b.md" if y else "c.md', producing ERR_INCLUDE_NOT_FOUND with a confusing, internals-leaking path instead of a clean dynamic-unresolved diagnostic. Reproduced deterministically 3/3 runs via `cargo run -p sc-compose -- render --mode file`.
- Requirement / ADR
- docs/requirements.md FR-3 (Include Expansion) requires deterministic expansion order and that "Include failures must produce actionable diagnostics with include-chain context." No requirement or ADR currently covers N-way (3+) ternary chaining inside @<{{ ... }}>; the only documented/tested conditional shape is the single-level "a" if cond else "b" form (crates/sc-composer/src/include.rs::conditional_path_candidates_are_exhaustive_and_renderable).
- Requirement / ADR follow-up
- No new requirement or ADR is needed -- this is a narrow parser-correctness gap inside an already-specified feature (conditional-candidate enumeration), not a request for a new chained-ternary feature. Recommend hardening quoted_literal()/parse_conditional_expression() to reject (fall back to Dynamic) any literal whose de-quoted content still contains " if " or " else ", so unsupported chaining fails with ERR_INCLUDE_DYNAMIC_UNRESOLVED per FR-3's actionable-diagnostics clause instead of resolving to a garbage path.
- Root cause
- crates/sc-composer/src/include/directive.rs: quoted_literal() accepts any first/last-quote-matched span as a literal without checking for embedded Jinja control keywords once dequoted; parse_conditional_expression() only performs a single split_once(" if ")/split_once(" else ") pass, so 3+-way ternary chains fall through to this over-permissive literal check instead of being rejected as Dynamic.
- Recommended fix
- Add a guard in quoted_literal() (or its caller in parse_conditional_expression) rejecting any candidate literal whose content contains " if " or " else " after unquoting, forcing multi-way ternary chains to resolve to IncludeDirective::Dynamic and surface ERR_INCLUDE_DYNAMIC_UNRESOLVED. Add a regression test in crates/sc-composer/src/include.rs asserting parse_include_directive() on a 3-way chained ternary returns Dynamic, not a Conditional with a garbled candidate.