Skip to content

feat(scenario): let a scenario declare a component once and repeat it over data - #174

Merged
LeadcodeDev merged 1 commit into
mainfrom
feat/templates-iteration
Aug 11, 2026
Merged

feat(scenario): let a scenario declare a component once and repeat it over data#174
LeadcodeDev merged 1 commit into
mainfrom
feat/templates-iteration

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes the "parameterised templates and data iteration" gap (High/M), aimed at the failure mode the original audit named as dominant: a generator duplicating a repeated structure by hand, every copy an opportunity to drift.

Ten near-identical cards used to mean ten JSON subtrees written out, each able to diverge on a colour, a font-size, a stray position. There was no way to say "this card, ten times, with this data".

The shape

{
  "components": {
    "stat_card": {
      "params": {
        "label":  { "type": "string" },
        "value":  { "type": "number", "default": 0 },
        "accent": { "type": "string", "default": "#6366F1" }
      },
      "template": {
        "type": "card",
        "style": { "width": "300px", "height": "160px", "background": "#111827" },
        "children": [
          { "type": "text",    "content": "$label", "style": { "color": "#94A3B8" } },
          { "type": "counter", "from": 0, "to": "$value", "style": { "color": "$accent" } }
        ]
      }
    }
  },
  "scenes": [{
    "duration": 3.0,
    "layout": { "direction": "row", "gap": 24, "justify_content": "center" },
    "children": [{
      "for-each": [
        { "label": "Revenue", "value": 1250, "accent": "#22C55E" },
        { "label": "Users",   "value": 340,  "accent": "#3B82F6" },
        { "label": "Growth",  "value": 8,    "accent": "#F59E0B" }
      ],
      "template": { "use": "stat_card", "props": { "label": "$label", "value": "$value", "accent": "$accent" } }
    }]
  }]
}

params reuses config's exact shape — a component parameter is a variable scoped to one instance instead of the whole file. Omitting default makes it required.

Why the overrides key is props and not config. Not an inconsistency: variables::substitute deliberately skips any object carrying a literal config key, to protect the root declarations block. Reusing that name would have left every for-each nested inside a use silently unsubstituted — precisely the failure mode this work exists to remove.

The index trap, answered the way this repo already answered it

Violation paths carry resolved indices while --fix navigates raw JSON. An iteration over ten items shifts everything after it by nine — the same defect include has, in a worse form.

--fix refuses, via FixRefusal::UsesTemplateDirectives alongside the existing UsesInclude. PR #145 established that refusal and #160 confirmed doing better is impractical. An end-to-end test drives cmd_validate --fix over an iterating scenario with a real geometry violation and asserts the file comes back byte-identical.

The validator sees the expanded tree, so geometry is checked against what actually renders.

Nothing fails silently

A cycle reports the chain (a -> b -> a) rather than overflowing the stack. Iterating a non-array reports what it found instead, and flags a $xxx-shaped value as a probable unresolved reference. An unknown component, a missing required parameter and an undeclared prop key each say so, with the structural path. A for-each that quietly produced nothing because a key was misspelled would be the worst available outcome.

Pass order, fixed and tested

Substitution → expansion → include, per document, each included file running the same pipeline independently. Consequences, both tested:

  • A for-each can iterate an array that came from a config variable or --var.
  • components is strictly file-local: reaching into an included file's definitions is a named error, in both directions, rather than an accident of scope.

One correction to the delivered work

The pre-expansion unresolved-reference scan reported every template binding as a typo — six warnings on the canonical example above, each telling the author they had misspelled a variable that in fact resolves perfectly.

That is not cosmetic. For a feature aimed at generated scenarios, a warning that is reliably wrong teaches the reader to ignore warnings, which would have undermined every genuine diagnostic this chantier has added — including the ones in this PR.

The scan now skips the directive bodies (template, props, components), matching the precedent already in that function for config, and runs again after expansion, where those keys are gone and a leftover $name is unambiguous. Both directions are pinned by tests: a correct binding warns about nothing, a misspelled $labl is still reported.

Verification

  • cargo test --workspace: 27 targets, 1078 tests, 0 failures
  • cargo fmt --all --check and cargo clippy --workspace --all-targets -- -D warnings: clean
  • Examples: 7 of 8 validate; the eighth is examples/ferriskey-presentation.json does not pass rustmotion validate (pre-existing on main) #157, pre-existing on main
  • The authored-with-for-each tree and the hand-written equivalent resolve to identical ResolvedScenario children, compared through the real loader — the only proof that factoring changes nothing about the render

Not covered

  • Iteration at the scenes[] level — the named case ("ten cards") is a children problem, and touching SceneEntry's bespoke deserializer would have widened the surface without a demonstrated need.
  • rustmotion schema does not document the new vocabulary: these keys are consumed before Scenario is deserialized, so they never appear in the struct schemars reads. No functional impact, but a schema-driven autocomplete would not suggest them.
  • rustmotion-studio edits by indexed JSON pointer and inherits the same index risk as --fix. Out of the listed scope, flagged rather than discovered later.
  • CLAUDE.md and the skills rules do not yet document the syntax.

… over data

Closes the "parameterised templates and data iteration" gap, aimed at the
failure mode the original audit named as dominant: a generator duplicating a
repeated structure by hand, every copy an opportunity to drift.

Ten near-identical cards used to be ten JSON subtrees written out. Now:

    "components": { "stat_card": { "params": {...}, "template": {...} } },
    "children": [{ "for-each": [ {...}, {...}, {...} ],
                   "template": { "use": "stat_card", "props": {...} } }]

`params` reuses `config`'s exact shape, so a component parameter is a
variable scoped to one instance instead of the file. Omitting `default`
makes it required.

The overrides key is `props`, not `config`, and that is not an
inconsistency: `variables::substitute` deliberately skips any object
carrying a literal `config` key, to protect the root declarations block.
Reusing the name would have left every `for-each` nested inside a `use`
silently unsubstituted — the failure mode this work exists to remove.

**`--fix` refuses on these scenarios**, exactly as it already does for
`include`. Violation paths carry resolved indices while `--fix` navigates
raw JSON; an iteration over ten items shifts everything after it by nine.
PR #145 established the refusal for `include` and #160 confirmed doing
better is impractical. Patching the wrong node silently is the one
unacceptable answer.

Every failure gets a named error saying where: a cycle reports the chain
rather than overflowing the stack, iterating a non-array reports what it
found instead, an unknown component, a missing required parameter and an
undeclared prop key all say so. A `for-each` that silently produced nothing
because a key was misspelled would be the worst possible outcome here.

Pass order is fixed and documented: substitution, then expansion, then
`include`, per document. So a `for-each` can iterate an array that came from
a `config` variable, and `components` is strictly file-local — reaching into
an included file's definitions is a named error in both directions rather
than an accident of scope.

One correction to the delivered work: the pre-expansion unresolved-reference
scan reported every template binding as a typo — six warnings on the
canonical example, each accusing the author of a mistake they had not made.
Warnings that are reliably wrong teach the reader to ignore warnings, which
would have cost more than the scan is worth, and would have undermined every
real diagnostic this chantier added. That scan now skips the directive
bodies, and runs again after expansion where the keys are gone and a
leftover `$name` is unambiguous. Both directions are tested: a correct
binding warns about nothing, a misspelled one is still caught.
@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Aug 11, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 11, 2026
@LeadcodeDev
LeadcodeDev merged commit 19e2022 into main Aug 11, 2026
3 checks passed
@LeadcodeDev
LeadcodeDev deleted the feat/templates-iteration branch August 11, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant