视觉画布设计
canvas-design
基于设计原则创作静态视觉作品,用于海报与品牌图形。
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…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
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 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.
The DSL pays for itself only above a threshold number of artifacts. Decide explicitly before building anything.
Count the artifacts: how many exist today, how many are expected over the corpus lifetime.
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. |
Check the override signals that favor a DSL regardless of count:
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.
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.
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."symmetry": "bilateral" flag duplicates every layer marked "mirror": true x-negated — half of a symmetric artifact's points come for free._intent field the interpreter ignores: one line stating what the recipe depicts, for human audits and reviewer diffs.{
"_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.
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.
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 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.
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.
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.
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.
# 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) { ... }
stop(), never fall through to a default — silent fallbacks let malformed recipes pass the gate.See 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.
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.
--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.--fast is for corpus-wide sweeps (completion checks, CI on the whole tree) — never the only gate a new recipe sees.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.
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.
spark mark and dodge the base item's gesture, so variant ≠ base at a glanceExpected: 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.
Generator agents emit one recipe per item and gate themselves before returning.
recipes/<slug>.json per assigned item; run the FULL validator on your own batch; only validated recipes count as done.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.
The validator proves renderable, not recognizable. Close the loop with human/agent judgment over real renders.
_intent and the conventions (orientation, family motifs, variant marks).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.
--fast mode (compile + bounds only) sweeps the full corpus within CI-tolerable timered_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.--fast for corpus sweeps; gate every new batch in full mode.