RFC-005 — YAML round-tripping
Status · Decided · closed 2026-04-30 → ADR-021 TA anchor · contracts/recipe-yaml · components/web-frontend Related · PRD-003 Recipe Editor · UXS-003 Recipe Editor · RFC-001 Recipe YAML schema Closes into · ADR-021 Why this is an RFC · The Recipe Editor's flip between Creative mode and YAML mode is a core artistic surface (PRD-003). Creative mode generates YAML; YAML mode allows direct edits. When a power user changes a value in YAML and flips back to Creative, what happens? The deliberation is not whether to support this — both modes must exist (PRD-003 sharpest threat) — but how the round-trip handles divergence between the two representations.
The question
The editor maintains two representations of the same recipe:
- Creative state — five values held in the editor's UI:
mood,energy_x,energy_y,colour_character,temporal_weight. - YAML representation — the full recipe file with structural fields above the marker and creative state + technical params (
render,audio) below the marker (per RFC-001 v0.2).
Going creative state → YAML is straightforward: the deterministic mapping function takes creative-state values and produces the render and audio blocks. The editor writes the creative-state values as YAML keys (mood:, energy_x:, etc.) and the derived blocks below them. All of these lines render amber in YAML mode.
Going YAML → creative state is harder. If a power user edits the YAML directly — changes colormap from thermal-warm to arctic-cool, or sets a particle-count override — what should the creative controls do when the user flips back to Creative mode?
There is no perfect answer. The mapping creative-state → technical-params is many-to-one (multiple creative states can produce similar technical values for some parameters), and the YAML edit may be a small tweak (still maps to a creative state) or a fundamental change (no creative state matches).
Use cases
- Small tweak in YAML. Power user changes
render.opacityfrom0.71to0.74. Almost no effect on creative state — the energy×presence point would barely move. The creative controls should snap to the closest match. - Large change in YAML. Power user replaces the entire
renderblock with hand-tuned values — different colormap, different blend mode, different particle behaviour. No nearby creative state exists. - Round-trip preservation. User opens an existing recipe, never edits YAML, just adjusts creative controls and saves. The YAML's
render/audioblocks should match exactly what the creative-to-yaml-mapping would produce, preserving git diffs at minimum. - Sketch override active. Recipe has
sketch_overrideset to a path. The technical block is ignored by the pipeline. What do creative controls show?
Goals
- A pure creative-mode workflow produces stable YAML across sessions (no spurious diffs).
- A power user editing YAML never has their work silently overwritten by creative-mode interactions.
- The user can always tell, by looking at the editor, whether the current creative-control state matches the YAML or has diverged.
- The same mapping function generates creative-state → YAML in the editor and the pipeline (no separate "interpretation" of the YAML happens at render time).
Constraints
- Determinism — creative-state-to-technical mapping must be a pure function. Same creative state always produces the same technical values (TA constraints).
- File-based storage — recipes are flat YAML; the editor cannot rely on hidden metadata in a database (TA constraints).
- Flat schema with marker — the schema (per RFC-001 v0.2) is flat; round-tripping operates on lines below the marker, not on a separate
creative:block. - No editor-only sketch code — the round-tripping logic lives in the editor, not in the sketch.
Proposed approach
Inverse-mapping with a "custom" indicator. Two-way binding for clean cases; explicit "custom" marker when divergence is detected. The logic operates on lines below the comment marker (per RFC-001 v0.2).
The editor keeps a deterministic mapping function creative_to_technical(creative_state) → technical_dict. This function is the source of truth for forward conversion. It is pure, versioned, and identical in editor and pipeline.
For YAML → creative state, the editor parses the file and partitions it at the marker:
parsed_yaml.below_marker:
creative_state C = (mood, energy_x, energy_y, colour_character, temporal_weight)
technical_blocks T = {render: {...}, audio: {...}}
expected_T = creative_to_technical(C)
if T == expected_T:
# Clean recipe. Creative controls show C exactly.
state = "matched"
elif T differs from expected_T only in a small number of fields:
# Power user tweaked some values. Creative controls show C.
# Tweaked fields are highlighted in YAML view as user-edits (slightly brighter amber).
state = "partially-custom"
else:
# Major divergence. Creative controls show C but greyed out;
# a "custom" badge appears.
state = "custom"
The user can always:
- Click "Reset to creative" — overwrites the technical blocks with
creative_to_technical(C). Loses YAML edits; warns first. - Click "Adopt YAML as creative" — finds the closest creative state that produces the current technical blocks (using a distance function on the mapping output) and updates
Caccordingly. May be lossy; warns first. - Continue working in
customstate — creative controls are read-only; YAML edits remain the source of truth.
The state (matched, partially-custom, custom) is editor-side only. It is not persisted in the YAML file. Reopening the recipe re-runs the comparison.
For sketch overrides (use case 4), the editor displays creative controls greyed out with a "sketch override active" message. The five creative-state YAML keys (mood, energy_x, etc.) and the technical blocks (render, audio) are preserved as-is but unused by the pipeline.
Alternatives considered
Alternative: hash-based detection (store creative state hash in YAML)
Embed a hash of the creative state as a field in the YAML; if it doesn't match the recomputed hash, mark as custom.
Rejected because it adds non-semantic state to the recipe file. The recipe is the artist's authored work; sticking a hash in there to support editor logic is the wrong concern in the wrong file. The inverse-mapping approach achieves the same detection from data already in the recipe.
Alternative: never round-trip — YAML edits are one-way
Once a user edits YAML, the recipe is permanently in "custom" mode. Creative controls are disabled forever for that recipe.
Rejected because it punishes small tweaks. A user nudging one parameter shouldn't lose access to the creative surface. The partially-custom middle state is the right granularity.
Alternative: full inverse mapping (always derive creative from technical)
When loading a YAML, recompute creative state from the render/audio blocks via an inverse function. Always show the closest creative state. Ignore the creative-state YAML keys (mood:, etc.) entirely.
Rejected because the mapping is many-to-one — there is no unique inverse. The closest creative state may not be what the user intended (they may have made deliberate technical-only choices). Better to preserve the user's stated creative state (the explicit mood: etc. keys) and detect divergence than to second-guess it.
Alternative: lock the YAML mode behind a confirmation
Require a "Yes, I want to edit YAML directly" confirmation before allowing technical-block edits. Reduces accidental divergence.
Rejected because it makes the YAML mode feel hostile. PRD-003's sharpest threat is power users feeling infantilised; gating YAML edits behind a confirmation worsens that. The custom state already provides clarity without friction.
Trade-offs
- The "closest creative state" function for Adopt YAML as creative is lossy. The user may end up with creative-control values they didn't choose. Mitigation: confirm before adopting.
- Editor and pipeline must use the same mapping function. This is a code-sharing constraint — the mapping logic lives in a module imported by both. Keeps logic synchronised; means a bug in the mapping affects both contexts at once.
- State detection runs every time YAML is loaded. Cheap (parse + comparison) but a non-zero startup cost per recipe.
- The creative-state YAML keys (
mood:,energy_x:, etc.) are duplicate sources of truth with therender/audioblocks. They have to stay in sync — when out of sync, the state-detection algorithm picks up the difference and flags it aspartially-customorcustom. This is the feature, not a bug, but it's worth being explicit that the editor must always update both the creative-state keys and the derived blocks together.
Open questions
- How small a divergence is
partially-customvscustom? A single field edit is clearly partially-custom; a complete rewrite is clearly custom. Where between? Probably a fixed threshold (e.g., >50% of fields differ). - Should there be a visible indicator in the gallery for recipes in
customstate — a small marker that this is a power-user piece? Could enrich the gallery with a useful signal; could also be visual noise. - The mapping function will evolve (new presets, refined energy×presence behaviour). A recipe authored in v1 should still load correctly in v2. Versioning the mapping function with a
creative_mapping_versionfield in the recipe? Probably worth it. - When the marker comment is missing (e.g., user deleted it accidentally), should the editor (a) restore it on save with the current mapping; (b) treat the entire file as custom; or (c) show a warning prompt? Probably (a) — restore silently, since the marker is editor-managed metadata.
How this closes
- ADR-NNN — Round-tripping and divergence handling. Locks the matched/partially-custom/custom state model, the explicit user actions (Reset, Adopt), the no-hash policy, and the mapping-function-versioning approach.
Links
- TA — contracts/recipe-yaml · components/web-frontend
- Related PRDs — PRD-003 Recipe Editor (this RFC was flagged in its open threads)
- Related UXS — UXS-003 Recipe Editor (visual contract for the YAML mode this RFC operates on)
- Related RFCs — RFC-001 Recipe YAML schema (the schema this RFC operates on)
Changelog
- v0.2 · April 2026 — Revised to match RFC-001 v0.2's flat schema. Replaced "loaded_yaml.creative / loaded_yaml.technical" partitioning with "lines below marker — creative-state keys + technical blocks (render, audio)". Added open question about marker-comment recovery on accidental deletion. Updated trade-offs to note the dual-source-of-truth between creative-state YAML keys and derived blocks.
- v0.1 · April 2026 — Initial draft, written assuming the two-tier (
creative:/technical:) schema from RFC-001 v0.1. Superseded by v0.2.