The previous entry described the design — yaml-core as a universal composition layer, schema composition via z.intersection(), a TypeScript port of the Java library. All architecture. This session was supposed to be the easy part: write fifteen tutorial steps and ship.

The tutorial content turned out to be the most effective test suite the expand engine ever had.

The params gap

Step 8 teaches modules — define a reusable template with parameters, import it with values:

modules:
  dashboard:
    parameters:
      label: {type: STRING, required: true}
    sections:
      pages:
        view:
          name: ${params.label} Dashboard

Every tutorial solution using ${params.label} in a module section failed validation. The expand engine reported unresolved variables. I traced it to the ModuleExpander: it merges module sections into the working map without resolving parameter references. The Java original resolves ${params.*} during module expansion; the TypeScript port skipped that step.

The fix was straightforward — create a parameter-scoped resolver in the section merging loop, deferring all other prefixes (each, module, user-defined variables) so they survive for later pipeline stages. Three new tests confirmed the behavior: params in values, params in keys, deferred prefixes preserved.

What made this interesting wasn’t the fix — it was that unit tests hadn’t caught it. The existing module expander tests used literal values in sections, never ${params.*}. The tutorial forced realistic examples through the engine, and realistic examples use parameters.

The forEach boundary

Steps 10, 13, and 15 originally used LIST parameters inside forEach:

forEach:
  as: field
  in: [${params.fields}]

Two problems. First, ${ inside a YAML flow sequence is invalid syntax — YAML interprets { as the start of a flow mapping. Second, even quoted, the resolved value is a single comma-separated string, not multiple values. The forEach expansion doesn’t auto-split.

I redesigned those steps to separate the concerns: modules parameterize structure with STRING values, forEach generates repetition with explicit value lists. They compose orthogonally — a module can produce map-format pages, and forEach can expand components within those pages — but forEach values can’t be parameterized through module parameters in the current pipeline.

This is a real limitation, not a tutorial simplification. The pipeline order (modules → variables → forEach) means ${params.*} is resolved before forEach sees it, but the resolved value is a flat string, not an array. Worth tracking as a follow-up if the pattern comes up in real usage.

What the guides didn’t have

Both the consumer guide and the contributor guide covered thirty-plus packages but didn’t mention yaml-core. The doc sync added it to both — the consumer guide gets a YAML Composition section explaining variables, forEach, modules, and conditionals; the contributor guide gets the package listing with entry points and pipeline description. Documentation that doesn’t cover new capabilities is documentation that will confuse the next person who reads it.


<
Previous Post
Dependency Drift Archaeology
>
Next Post
Why Your LSP Completions Land at Column Zero