# Generative Recipe Dsl

> Design a generative recipe DSL so agents emit small validated JSON documents and ONE audited interpreter renders them all — data, not code. Covers the N-artifacts threshold for choosing a DSL over bespoke code, designing roles as style slots so a single recipe renders unchanged in every output style, the validator gate (compile + bounds + real render in all styles, plus a --fast mode for corpus-wide sweeps), hand-authored seed recipes, and the emergent conventions that keep a corpus of hundreds consistent without per-item steering. Use when many similar artifacts (icons, status badges, diagram nodes, test fixtures) must be generated by agents, when per-item bespoke code is multiplying unreviewable code paths, or when a whole corpus must be re-rendered cheaply into new styles.

- Skill: `pjt222/generative-recipe-dsl` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add pjt222/generative-recipe-dsl`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pjt222/generative-recipe-dsl/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: pjt222 (https://skillmd.com/u/pjt222)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/pjt222/generative-recipe-dsl

---


# Build a Generative Recipe DSL

When many similar artifacts must be generated — icons, badges, diagram nodes, test fixtures — having each generator write bespoke code does not scale: N code paths to review, no uniform validation, no cheap re-render. A recipe DSL inverts this: generator agents emit small JSON documents (recipes), and ONE interpreter renders them all. The interpreter is the single audited code path; the recipes are pure data, gated by a validator before any item counts as done. The pattern is proven at corpus scale: in the originating trading-card glyph project, over a thousand agent-generated recipes render through one ~120-line interpreter in two completely different styles.

This skill is the complement of [create-glyph](../create-glyph/SKILL.md): create-glyph hand-authors ONE glyph as R code with individual craft; generative-recipe-dsl builds the machinery for MANY agent-generated artifacts as data under one interpreter. Reach for create-glyph when a single artifact deserves bespoke attention; reach for this skill when a corpus is coming.

## When to Use

- A family of roughly 15 or more similar artifacts must be produced: icons for a skill domain, status badges for a service fleet, node pictograms for a diagram set, fixtures for a test matrix
- Generator agents author the artifacts — small JSON documents can be machine-validated at corpus scale; N bespoke code files cannot be reviewed at corpus scale
- The corpus must render in more than one style (print + screen, mono + color, light + dark), or a future restyle is plausible
- Per-item bespoke code is already multiplying unreviewed code paths for artifacts that share one visual or structural grammar
- Do NOT use for one-off or heterogeneous artifacts whose logic is genuinely unique — hand-author those (for icons, use [create-glyph](../create-glyph/SKILL.md))

## Inputs

- **Required**: Artifact family and expected corpus size (what is being generated, how many now, how many later)
- **Required**: At least two target render styles (e.g. mono engraving for print + flat color for screen) — two styles are what prove the role slots
- **Required**: Host toolchain for interpreter and validator (e.g. R/ggplot2, Python/SVG, JavaScript/Canvas)
- **Optional**: Existing bespoke implementations to mine for the primitive vocabulary
- **Optional**: Batch driver for wave generation — the companion workflow [batch-generate-waves](../../workflows/batch-generate-waves.mjs)

## Procedure

### Step 1: Decide — Recipe DSL or Bespoke Code

The DSL pays for itself only above a threshold number of artifacts. Decide explicitly before building anything.

1. Count the artifacts: how many exist today, how many are expected over the corpus lifetime.
2. Apply the threshold:

   | Artifact count | Recommendation |
   |---|---|
   | 1–3 | Bespoke code. A DSL costs more than it saves. |
   | ~4–15 | Gray zone. DSL if the artifacts share one shape grammar and more are expected; bespoke if each needs unique logic. |
   | 15+ | Recipe DSL. Reviewing N code paths costs more than one ~120-line interpreter; validation and re-render become uniform. |

3. Check the override signals that favor a DSL regardless of count:
   - The generators are agents — emitted data can be gated mechanically; emitted code cannot
   - More than one render style is needed now, or a restyle is plausible later
   - The whole corpus must stay cheaply re-renderable (style tweaks touch one file, not N)
   - Shared conventions must hold corpus-wide (see Step 6)
4. Check the counter-signals: each item genuinely needs unique logic, there is no common primitive vocabulary, or the corpus will never grow. If these dominate, stop here and write bespoke code.

**Expected:** A written go/no-go decision naming the artifact count, the styles, and which signals tipped it.

**On failure:** If the decision is unclear, prototype 2–3 artifacts both ways. If the bespoke versions share most of their structure and differ only in data-like parameters, the DSL is viable — that shared structure IS the interpreter.

### Step 2: Design the Recipe Schema

A recipe is data, not code: a small JSON document an agent can emit in one shot and a validator can check mechanically. The proven shape is **primitives × roles × modifier flags**.

1. Enumerate the shape primitives from your prototypes. Keep the set small — the reference implementation shipped with 7 (`polygon`, `ngon`, `circle`, `star`, `arc`, `zigzag`, `rect`), grew to 8 only when `spiral` was added centrally after launch, and covered a corpus of over a thousand recipes.
2. Fix a local coordinate frame with a soft convention and a hard limit the validator enforces (reference: content within ±36 units, hard limit ±45, +y is up).
3. Add cheap-leverage modifiers. The highest-value one is bilateral symmetry: a top-level `"symmetry": "bilateral"` flag duplicates every layer marked `"mirror": true` x-negated — half of a symmetric artifact's points come for free.
4. Include a free-text `_intent` field the interpreter ignores: one line stating what the recipe depicts, for human audits and reviewer diffs.

```json
{
  "_intent": "one-line human note: what this artifact depicts",
  "symmetry": "bilateral",
  "layers": [
    {"shape": "rect", "x0": -30, "y0": -20, "x1": 30, "y1": 20, "role": "ground"},
    {"shape": "circle", "cx": 22, "cy": 13, "r": 4, "role": "spark"},
    {"shape": "arc", "cx": 0, "cy": 0, "r": 31, "from": 42, "to": 78, "role": "line", "mirror": true}
  ]
}
```

**Expected:** A schema document (one page, kept next to the recipes) listing every primitive with its parameters, the coordinate frame, the modifiers, and the roles from Step 3.

**On failure:** If prototypes keep demanding new primitives, the family may be too heterogeneous for one DSL — split into two schemas, or return to Step 1. If one primitive is repeatedly requested by generators (the reference implementation added `spiral` this way), add it centrally through code review, never per-item.

### Step 3: Design Roles as Style Slots

Roles are the load-bearing design decision. A role names the layer's FUNCTION in the composition — never its appearance. Each output style then maps every role to concrete aesthetics, so one recipe renders in every style unchanged. The reference implementation renders the same JSON as a mono engraving (ink, paper, hatching) and as flat 8-bit color from exactly this indirection.

1. Define roles by compositional function. The proven six-slot vocabulary:

   | Role | Function in the composition | Mono engraving style | Color style |
   |---|---|---|---|
   | `ground` | large backdrop mass | paper fill + ink outline + 45° hatch | base fill + frame outline |
   | `mass` | primary solid body | solid ink | body fill + frame outline |
   | `accent` | secondary textured region | paper fill + ink outline + 135° hatch | highlight fill + frame outline |
   | `spark` | small highlight marks | solid ink | signal fill, no outline |
   | `detail` | cutout on a solid (eyes, keyholes) | solid paper | signal fill, no outline |
   | `line` | stroked path, no fill | soft ink path | highlight path |

   How a style separates two roles is the style's choice, not the schema's: the mono column shows the reference mapping (`accent` hatches at 135° vs `ground`'s 45°), while the worked badge example in [references/EXAMPLES.md](references/EXAMPLES.md) separates `accent` from `ground` by tighter hatch spacing at the same angle. Both are valid slot mappings — the distinction lives in the style, never in a recipe.

2. Apply the litmus test: propose a hypothetical third style (say, blueprint linework). If every role maps cleanly to new aesthetics without touching any recipe, the slots are functional. If a role only makes sense in one style, it is an appearance name in disguise — rename it by function.
3. Encode substrate interactions as conventions, not new roles: e.g. `spark` marks read on hatched/`ground` bodies; `detail` cutouts read on solid `mass` bodies. Pick the mark's role for the body under it.

**Expected:** A role table mapping each role to concrete aesthetics in EVERY target style; the third-style litmus test passes on paper.

**On failure:** If two roles map identically in all styles, merge them. If a needed look has no role, first try composing existing roles (outline + hatch = two layers) before adding a seventh slot.

### Step 4: Write the Interpreter — One Audited Code Path

The interpreter compiles a recipe into render layers. It is the ONLY code that turns recipes into artifacts — audit it once, and every generated item inherits the audit.

1. Structure it as three small functions plus a loader (the verified reference is ~120 lines total):

```r
# geometry: layer spec -> points; unknown shape is a HARD error, never skipped
badge_layer_geometry <- function(layer) {
  if (layer$shape == "circle") return(poly_regular(layer$cx, layer$cy, layer$r, 48))
  # ... one branch per primitive ...
  stop("dsl: unknown shape '", layer$shape, "'", call. = FALSE)
}

# style slots: (geometry, role, style) -> drawable layers; unknown role is a HARD error
badge_role_layers <- function(geometry, layer, style) { ... }

# compile: recipe -> function(cx, cy, s, style), applying symmetry + placement
badge_from_spec <- function(recipe) { ... }
```

2. Enforce vocabulary hard: unknown shape or role must `stop()`, never fall through to a default — silent fallbacks let malformed recipes pass the gate.
3. Keep it deterministic: no RNG, no time dependence. The same recipe must render byte-identically on every run, or corpus-wide re-renders produce untrackable diffs.
4. Freeze the contract: generator agents may never edit the interpreter. Primitive additions go through normal code review as central changes.

See [references/EXAMPLES.md](references/EXAMPLES.md) for a complete, empirically verified ~170-line interpreter in a non-icon domain (service status badges).

**Expected:** The interpreter compiles hand-written test recipes into layers in every style; a recipe with an unknown shape or role raises a hard error.

**On failure:** If the interpreter is growing past ~250 lines, per-item logic is leaking in — push variation back into recipe data or drop a primitive. If styles disagree on layer counts, a role branch is emitting different structure per style; roles must vary aesthetics only.

### Step 5: Write the Validator Gate

The validator is what makes agent-authored data trustworthy: no recipe counts as done until it passes. It must end in a REAL render — compile-only checks pass recipes that crash the renderer.

1. Implement the checks in order, per recipe × per style:
   1. **Parses** — strict JSON, no recovery
   2. **Structural floor** — e.g. at least 2 layers
   3. **Compiles** — the interpreter accepts every layer (this catches unknown shapes/roles)
   4. **Bounds** — all geometry within the hard coordinate limit
   5. **Real render** — write an actual output file in EVERY style; assert plausible file size
2. Exit nonzero on any failure and print one PASS/FAIL line per recipe — the gate must be usable by agents and CI alike.
3. Add a `--fast` mode that runs checks 1–4 and skips the render. Rendering is the slow O(N) step: in the reference implementation `--fast` sweeps a ~1,200-recipe corpus in ~100 seconds where per-item rendering would time out.
4. Fix the usage pattern: **full mode gates every new batch; `--fast` is for corpus-wide sweeps** (completion checks, CI on the whole tree) — never the only gate a new recipe sees.

```bash
Rscript validate_badges.R message-queue auth-gateway   # named recipes, full gate
Rscript validate_badges.R                              # all recipes, full gate
Rscript validate_badges.R --fast                       # whole corpus: compile + bounds only
```

**Expected:** Valid recipes print PASS in both modes; a deliberately broken recipe (unknown role, out-of-bounds point) prints FAIL and the process exits 1.

**On failure:** If the full gate is too slow even per-batch, render to a throwaway raster at reduced size — but keep a real render. If the validator and interpreter disagree (validator passes what the renderer rejects), the validator is re-implementing rules instead of calling the interpreter; make it call the same compile path.

### Step 6: Hand-Author Seed Recipes and Name the Conventions

Seed recipes are the corpus's style anchor. Once the interpreter plus a few seeds exist, conventions hold across hundreds of generated items without per-item steering — the DSL constrains agents into consistency.

1. Hand-author 3–8 seed recipes that together exercise every primitive and every role, and pass the full gate.
2. Start a conventions document next to the recipes. Conventions observed to emerge and hold corpus-wide in the reference implementation:
   - **Family motifs**: related items share ONE motif elaborated across variants (an evolution line elaborating the same shape; a service family sharing an envelope motif)
   - **Owner/variant marks**: variants of a base item carry a small shared `spark` mark and dodge the base item's gesture, so variant ≠ base at a glance
   - **Substrate rules**: which mark role reads on which body role (Step 3.3)
   - **Orientation rules**: +y is up; a recipe built y-down still validates but renders upside down — the gate cannot catch it
3. Feed the schema doc, the seeds, and the conventions into every generator prompt.

**Expected:** All seeds pass the full gate; the conventions document exists and is referenced by the generator prompt.

**On failure:** If seeds look inconsistent with each other, fix that before generating — agents amplify whatever the seeds do, including their inconsistencies.

### Step 7: Generate at Scale

Generator agents emit one recipe per item and gate themselves before returning.

1. Fix the agent contract: read the schema doc + seeds + conventions; emit `recipes/<slug>.json` per assigned item; run the FULL validator on your own batch; only validated recipes count as done.
2. Derive filenames from one canonical slug function (a single source of truth shared by generators and coverage checks) — ad-hoc slugging orphans items silently.
3. For corpus-scale runs, drive waves with the companion workflow [batch-generate-waves](../../workflows/batch-generate-waves.mjs) (scout the remaining pool → generate a wave, validator-gated → audit). Because recipes land on disk before a wave returns, an interrupted wave loses nothing: validate, commit the survivors, and the next scout pass skips them.
4. Keep the bespoke escape hatch: individually crafted artifacts remain allowed alongside recipes, with a fixed precedence rule (reference: bespoke wins over a recipe of the same name).

**Expected:** Waves of validated recipes accumulating in `recipes/`, each wave passing the full gate before merge.

**On failure:** If an agent's recipes fail the gate repeatedly, its prompt is missing the schema doc or conventions — do not patch the interpreter to accept them. If the scout under-enumerates the remaining pool (observed at low remaining counts), pre-compute the missing list with the slug function and pass it to the wave explicitly.

### Step 8: Audit the Corpus

The validator proves renderable, not recognizable. Close the loop with human/agent judgment over real renders.

1. Render proof sheets (paged contact sheets of the whole corpus) in each style.
2. Judge recognizability against each item's `_intent` and the conventions (orientation, family motifs, variant marks).
3. Track weak items in a backlog file (validates + renders but reads generic) rather than blocking the corpus; schedule a redraw pass.
4. Run a coverage/drift check whenever the item pool changes: every pool item's canonical slug must resolve to exactly one recipe or bespoke artifact; report misses and orphans.

**Expected:** Proof sheets reviewed in every style; coverage check reports zero missing and zero orphaned items; weak-item backlog exists.

**On failure:** If many items read generic, the primitive set or seed diversity is too thin — improve the seeds and conventions, then redraw the weak tail; do not hand-tune the interpreter for single items.

## Validation

- [ ] Go/no-go decision from Step 1 is recorded, with artifact count and style targets
- [ ] Schema document exists next to the recipes: primitives + parameters, coordinate frame, modifiers, roles
- [ ] Every role maps to concrete aesthetics in EVERY target style; the third-style litmus test passes
- [ ] One interpreter is the only render path; unknown shape or role raises a hard error (test with a deliberately broken recipe)
- [ ] Validator gate runs compile + bounds + real render in all styles and exits nonzero on any failure
- [ ] `--fast` mode (compile + bounds only) sweeps the full corpus within CI-tolerable time
- [ ] 3+ hand-authored seed recipes pass the full gate and exercise every role
- [ ] Conventions document exists and is included in generator prompts
- [ ] Coverage check: every pool item resolves to exactly one recipe or bespoke artifact

## Common Pitfalls

- **Roles named by appearance**: `red_fill` or `hatched` instead of `spark` or `accent` breaks every recipe the moment a second style arrives. Name roles by compositional function; let each style map function to look.
- **Validator without a real render**: compile + bounds proves a recipe is well-formed, not that it renders — device errors, degenerate geometry, and style-mapping bugs only surface in a real render. Keep `--fast` for corpus sweeps; gate every new batch in full mode.
- **Trusting PASS for recognizability**: an upside-down or unrecognizable recipe validates fine. The gate checks mechanics; proof-sheet audits check meaning. Budget for both.
- **Interpreter creep**: generator agents "helpfully" extending the interpreter destroys the single-audited-code-path property. Freeze it; route primitive requests through central code review (the reference added exactly one primitive this way, after it became the most-requested shape across waves).
- **One-off schema extensions**: a per-item parameter added for one recipe becomes unvalidated dead weight. Express the need with existing primitives, or extend the schema centrally with validator support.
- **ID/filename drift**: recipes named by ad-hoc slugging silently orphan items from their pool entries. Use one canonical slug function as the single source of truth and run the coverage check after any pool change.

## Related Skills

- [create-glyph](../create-glyph/SKILL.md) — the complement: ONE hand-authored glyph with individual craft, where this skill generates MANY recipes under one interpreter
- [enhance-glyph](../enhance-glyph/SKILL.md) — the polish pass for individual items flagged weak in the Step 8 audit
- [dream](../dream/SKILL.md) — the generative front end: unconstrained exploration produces the concept a recipe then encodes (a dream becomes a recipe becomes a picture)
- [stale-proof-rendered-numbers](../stale-proof-rendered-numbers/SKILL.md) — the sibling data-not-code pattern for rendered figures: numbers substituted from measured data, with the build failing on missing keys
- Companion workflow: [batch-generate-waves](../../workflows/batch-generate-waves.mjs) — the resumable scout → generate → audit batch driver for which this skill defines the canonical per-item generator

