ember

11Error analysis

The run has 13 misses. Each card shows the evidence ember saw, the gold label and its rationale, ember's full answer, and what an agent following the kit would have done with it. Reviewers can use this section as the label-review queue.

What the kit does with the missMisses
deferred: gather evidence or ask8
acted, flagged for verification3
acted on a wrong answer2

note_placement-art-palettes: placement

Note placement · dev
Gold
Place the note under Art/ (Art)
ember
Place the note under Design/ (Design)
Signal
confidence 0.38 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Art/Palettes.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Palettes

The biome palette decisions for Old Growth. Each biome has a distinct color regime drawn from the biome's tonal purpose. Palettes are not decorative choices — they are the primary carrier of biome identity and the most visible expression of [[Biome Behavior]] across every system the player encounters.

## Overview

Old Growth's three biomes are distinguishable **by color temperature and saturation profile**, not by hue alone (Article XII-G). Each biome's palette is a design constraint every asset must honor. Danger/disturbance signals must never appear in neutral UI contexts; the palette vocabulary is strict.

## Design Intent

Per Article XII-G: "Biome palettes MUST be distinguishable by color temperature and saturation profile, not by hue alone." A colorblind-accessible design (deuteranopia/protanopia simulation must pass) requires palette distinction that works in greyscale. Value and saturation, not hue, carry identity.

## Key Decisions

### Verdant Old Growth — filtered gold + deep green

The condition at rest. Beauty at scale. The canopy is a ceiling that belongs to something else.

- **Dominant hues**: deep green, filtered gold
- **Color temperature**: warm (filtered sunlight)
- **Saturation**: medium-high on the greens; warm highlights
- **Value range**: wide — from deep shadow under the canopy to bright columns of golden light
- **Accent colors**: amber (Amber Bleed anomaly), bioluminescent undertones at dusk
- **Weather tint**: rain flattens the palette into cool wet greens; fog adds grey-green to the low ground

### Fungal Depths — blue-white + amber

The condition in conversion. The spore haze has replaced weather.

- **Dominant hues**: blue-white (bioluminescent mushrooms), amber (sap in hollows), deep shadow
- **Color temperature**: cool core, warm pools
- **Saturation**: high contrast between bioluminescent cool and amber warm against darkness
- **Value range**: primarily low (dark undergrowth) with bright bioluminescent accents
- **Accent colors**: acid green (mycelium at threshold), dim purple (deepest Fungal areas)
- **Weather tint**: none — the spore haze is the atmosphere. Visibility is reduced by haze, not fog.

### Petrifold — grey on grey

The condition at completion. Nothing is changing anymore.

- **Dominant hues**: cool grey, neutral grey, warm grey
- **Color temperature**: desaturated — near-monochrome
- **Saturation**: extremely low
- **Value range**: narrow — mostly mid-grey with subtle variation
- **Accent colors**: the Samba's exhaust and headlights are the only warmth, and they **look wrong here**. This wrongness is intentional.
- **Weather tint**: none. No wind, no rain. The air is dry and still in a way that reads less like calm and more like conclusion.

### UI palette (diegetic)

The dashboard uses **amber** (per the World Bible: "amber-glow instruments"). Amber belongs to the Samba. It does not appear in the world except at the Amber Bleed anomaly and the exit lanterns — both deliberately borrowing the Samba's visual language for diegetic confusion.

### Danger signals

- **Disturbance state expression** is not color-coded directly. It manifests through radio degradation, weather, anomaly density — all diegetic.
- **Warning colors** on any legible UI (dashboard gauges) must pair hue with value/icon per Article XII-G accessibility rules.

### Greyscale test

Every biome palette must pass greyscale legibility. Verdant's value range is wide; Fungal's depends on value contrast (bright points against darkness); Petrifold is intentionally narrow-value and relies on texture/composition. All three are distinguishable without hue.

## How It Appears In-Game

The transition between biomes is continuous. One biome's palette bleeds into the next before the player has consciously named the change. The color grading shifts gradually — a Verdant-to-Fungal transition is a slow desaturation of greens and an introduction of blue-white points of light. Fungal-to-Petrifold is the removal of color itself.

The Samba's colors (its paint, its amber dashboard glow, its headlights) remain constant across biomes. This constancy is itself the design: the Samba is the player's only color anchor, and the biomes' palettes read against that anchor.

## Biome ambient production values

The design intent above (colour temperature, saturation profile, value range) is implemented as specific hex and linear RGB production values in [[Art/Biome Ambient Palette]]. That note is the authority for:
- Godot `Environment.ambient_light.color` per biome
- Sky colour and fog tint per biome
- The cross-biome bus read table (how Samba hero colours shift under each ambient)
- All 9 values encoded in the `VanPalette` resource (slots 13–21, see [[Art/Vehicle — Samba Palette Resource]])

## Related

- [[Visual Direction]] — overall art direction
- [[Biomes]] — the three-register gradient
- [[Biome Behavior]] — cross-cutting concept thread
- [[Tone and Horror]] — the tonal purpose palettes serve
- [[Pacific Drive Reference]] — peer reference for palette-as-storytelling
- [[Rock Scatter Design]] — biome tinting applied to rocks
- [[Art]] — folder MOC
- [[Constitution]] — Articles IX and XII-G are the authoritative sources
ember's answer for note_placement-art-palettes, placementProbability ember gave each option.Art0.28 goldCode0.01Decisions0.23Design0.38 ember's pickDiscoveries0.01Governance0.03Narrative0.01Reference0.03Sessions0.00Systems0.02

note_placement-code-tools-music-pipeline: placement

Note placement · dev
Gold
Place the note under Code/ (Code)
ember
Place the note under Design/ (Design)
Signal
confidence 0.38 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Code/Tools — Music Pipeline.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Tools — Music Pipeline

The complete music pipeline for Old Growth — Suno → WAV → gold master → 13-station genre-specific post-processing → OGG ship. Consolidates 7 archived docs covering Suno track generation, download workflow, WAV conversion, per-genre ffmpeg filter chains, batch processing, and installation to the audio submodule. Distinct from the spoken TTS pipeline ([[Audio — Pipeline]] / [[Tools — TTS]]) which handles voice content.

> [!attention] PLANNED TOOLING
> The `tools/music/` directory and its associated Make targets and batch scripts described below are a **design specification** — they have not yet been implemented in the repository. Tracks are currently sourced manually per the workflow here and hand-processed through ffmpeg following the per-genre filter chains. Implementation of the automated pipeline is a future spec. All file paths beginning `tools/music/` in this note describe the target layout, not current code.

## Overview

Music content originates in **Suno (Pro or Premier for commercial rights)**. Tracks are generated from per-genre Suno prompts (see [[Audio — Radio Stations]] for complete prompt libraries per genre lane). Downloaded MP3s are converted to 48 kHz/24-bit WAV gold masters, post-processed through genre-specific ffmpeg filter chains to apply period-correct "radio filter" character, then encoded to OGG and installed into the audio submodule.

**Scale target**: 13 music stations × 15–20 tracks each = ~200–260 music tracks total.

## The 13 Music Stations

| Call Letters | Freq | Genre |
|---|---|---|
| KOLD | 88.1 | Dark Folk / Neofolk |
| KHRN | 90.1 | Haunted Americana / Gothic Singer-Songwriter |
| KFRO | 92.1 | Nordic Experimental |
| KDRM | 94.7 | Dream Pop / Ethereal |
| KDRN | 95.9 | Dark Ambient / Drone |
| KPGN | 97.3 | Pagan Folk / Ritual Ambient |
| KJAZ | 98.5 | Jazz |
| KVPR | 100.5 | Vaporwave / Retro Synth |
| KGSP | 102.1 | Dark Gospel / Sacred Americana |
| KBPK | 107.3 | Blues-Punk / Dirty Rock |
| KGNG | 105.9 | PNW Garage-Psych |
| (KTLK 93.3 is talk-format; not a music station) |
| (KNWS 103.7 is news; not a music station) |

See [[Audio — Car Radio]] for the full station roster including talk/news and frequency list. [[Audio — Radio Stations]] has the complete genre case study per lane with Suno prompt keyword libraries.

## Workflow: From Suno to Game-Ready

### Step 1 — Generate Music in Suno

- Use [[Audio — Radio Stations]] per-genre prompt keywords as the base Suno input
- Generate **multiple variants per prompt** (Suno's Pro plan allows batch generation)
- Target track length: 2–5 minutes depending on genre
- Generate at maximum Suno fidelity (Premier plan, stereo, 192+ kbps MP3)
- **Commercial license**: Pro or Premier plan explicitly required — Basic plan does NOT grant commercial rights

### Step 2 — Download Raw Audio from Suno

Suno exports as MP3 (Pro/Premier tier). Download via:
- Suno web UI (per-track download)
- Suno API (batch, if available on plan)

**Organize by station immediately**:
```
tools/music/raw/
├── kold/
├── khrn/
├── kfro/
├── kdrm/
├── kdrn/
├── kpgn/
├── kjaz/
├── kvpr/
├── kgsp/
├── kbpk/
└── kgng/
```

Filename convention:
```
{station_slug}_track_NN_{short_descriptor}.mp3
# e.g. kold_track_01_forest_isolation.mp3
#      kjaz_track_07_late_night_club.mp3
```

### Step 3 — Convert MP3 → WAV (48 kHz / 24-bit)

```bash
ffmpeg -i kold_track_01_forest_isolation.mp3 \
  -acodec pcm_s24le -ar 48000 -ac 2 \
  kold_track_01_forest_isolation.wav
```

Or batch:
```bash
cd tools/music/raw/kold
for mp3 in *.mp3; do
  ffmpeg -i "$mp3" -acodec pcm_s24le -ar 48000 -ac 2 "../gold/kold/${mp3%.mp3}.wav"
done
```

Gold masters land in `tools/music/gold/{station}/`.

### Step 4 — Listen & QA

Before post-processing, review each gold master:
- Does it match the Suno prompt's emotional intent?
- Are there Suno artifacts (abrupt endings, repetition glitches, vocal oddities)?
- Does the track fit the 2–5 minute target duration?
- Does it work in the context of the station's other tracks?

Reject tracks that don't pass QA. Regenerate in Suno with refined prompts.

### Step 5 — Apply Genre-Specific Processing

Each station has its own ffmpeg filter chain that applies the station's characteristic radio fidelity.

#### KOLD 88.1 — Dark Folk / Neofolk

Unsettling sparse acoustic. Period filter emphasizes baritone vocal reverb.

```
highpass=f=80
lowpass=f=7000
equalizer=f=250:width_type=o:width=1:g=-2   # scoop low-mids
equalizer=f=2500:width_type=o:width=1.5:g=2 # presence
compressor=threshold=-18dB:ratio=2:attack=5:release=200
libvorbis q:a=5
```

#### KHRN 90.1 — Haunted Americana

The "lonely highway" lane. Light tape-saturation, spring reverb emphasis.

```
highpass=f=60
lowpass=f=9000
aecho=0.6:0.7:80:0.2    # subtle spring-like echo
equalizer=f=180:width_type=o:width=1:g=1.5  # warmth
equalizer=f=3000:width_type=o:width=1:g=1
compressor=threshold=-16dB:ratio=2.5:attack=5:release=150
libvorbis q:a=5
```

#### KFRO 92.1 — Nordic Experimental

Hymn-like, reverent. Preserve wide dynamics.

```
highpass=f=40
lowpass=f=11000
equalizer=f=200:g=1                          # gentle low warmth
compressor=threshold=-14dB:ratio=1.5:attack=10:release=300  # gentle compression preserves dynamics
libvorbis q:a=5
```

#### KDRM 94.7 — Dream Pop

Lush, oceanic, reverb-drenched. Preserve high-end shimmer.

```
highpass=f=50
lowpass=f=12000
equalizer=f=3500:width_type=o:width=1:g=1.5  # air
equalizer=f=100:g=1                          # warmth
compressor=threshold=-18dB:ratio=2:attack=10:release=250
libvorbis q:a=5
```

#### KDRN 95.9 — Dark Ambient / Drone

Formless tension. Preserve sub-bass.

```
highpass=f=25
lowpass=f=10000
equalizer=f=60:g=2                           # sub-bass presence
compressor=threshold=-14dB:ratio=1.5:attack=20:release=500  # very gentle compression
libvorbis q:a=5
```

#### KPGN 97.3 — Pagan Folk

Ceremonial, ancient. Natural dynamics, minimal processing.

```
highpass=f=50
lowpass=f=10000
equalizer=f=150:g=1                          # wood warmth
# No compression — let it breathe
libvorbis q:a=5
```

#### KJAZ 98.5 — Jazz

Cool, sparse. Classic jazz radio character.

```
highpass=f=80
lowpass=f=8500
equalizer=f=400:width_type=o:width=1:g=-1    # scoop mud
equalizer=f=3000:width_type=o:width=1:g=1.5  # presence
compressor=threshold=-16dB:ratio=2:attack=5:release=200
libvorbis q:a=5
```

#### KVPR 100.5 — Vaporwave

Retro synth nostalgia. Preserve the synth character; add subtle tape wobble.

```
highpass=f=40
lowpass=f=12000
equalizer=f=80:g=1.5                         # warm lows
equalizer=f=4000:g=1
# Optional: vibrato=f=1.5:d=0.01             # very subtle tape wobble
compressor=threshold=-12dB:ratio=2.5:attack=5:release=150
libvorbis q:a=5
```

#### KGSP 102.1 — Dark Gospel

Warm, vocal-forward. Intimate room feel.

```
highpass=f=60
lowpass=f=9000
equalizer=f=250:g=1                          # vocal warmth
equalizer=f=2500:g=1.5                       # presence
compressor=threshold=-14dB:ratio=3:attack=5:release=200
libvorbis q:a=5
```

#### KBPK 107.3 — Blues-Punk

Greasy, mid-heavy. Deliberately front-loaded mix.

```
highpass=f=120
lowpass=f=7000
equalizer=f=800:width_type=o:width=1:g=2     # midrange punch
compressor=threshold=-10dB:ratio=4:attack=3:release=100  # aggressive compression
libvorbis q:a=5
```

#### KGNG 105.9 — PNW Garage-Psych

Loud, fuzzy, live-feel. Push into the red.

```
highpass=f=80
lowpass=f=9000
equalizer=f=2500:g=2                         # presence
compressor=threshold=-8dB:ratio=4:attack=3:release=80    # aggressive
libvorbis q:a=5
```

### Step 6 — Batch Processing Script

`tools/music/Makefile`:

```makefile
# Per-station batch processing
kold-batch:
	for wav in gold/kold/*.wav; do \
	  base=$$(basename $$wav .wav); \
	  ffmpeg -i $$wav \
	    -af "highpass=f=80,lowpass=f=7000,equalizer=f=250:g=-2,equalizer=f=2500:g=2,compressor" \
	    -c:a libvorbis -q:a 5 \
	    ogg/kold/$$base.ogg; \
	done

# (Repeat per station with the appropriate filter chain)

all-music-batch: kold-batch khrn-batch kfro-batch kdrm-batch kdrn-batch \
                 kpgn-batch kjaz-batch kvpr-batch kgsp-batch kbpk-batch kgng-batch
```

OGG outputs land in `tools/music/ogg/{station}/`.

### Step 7 — Install to Submodule

```bash
# Per station
make install-music STATION=kold

# All stations
make install-music-all
```

Installs gold masters + OGGs into `src/assets/audio/radio/music/{station}/` with provenance JSON (same schema as TTS pipeline per [[Audio — Pipeline]]).

## Manifest Autogeneration

Music tracks use a simpler manifest than TTS (no CSV per-clip needed — each gold master stands alone with provenance JSON).

However, **station-level manifests** exist: `tools/music/manifests/{station}.json` lists the tracks belonging to each station in cycle order:

```json
{
  "station": "KOLD",
  "frequency": 88.1,
  "genre": "Dark Folk / Neofolk",
  "tracks": [
    {
      "clip_id": "kold_track_01_forest_isolation",
      "title": "Forest Isolation",
      "duration_sec": 184,
      "cycle_position": 1
    },
    {
      "clip_id": "kold_track_02_walking_into_dark",
      "title": "Walking Into Dark",
      "duration_sec": 221,
      "cycle_position": 2
    }
  ]
}
```

Manifests are auto-generated from gold master directories via:

```bash
make music-manifests
```

Which scans `tools/music/gold/{station}/` and writes the manifest JSONs. Can be regenerated after adding/removing tracks.

## Gold-Folder Autodiscovery

The music pipeline supports **autodiscovery**: scan gold folders containing archival masters and automatically generate manifests without manual editing.

### Music Discovery Layout

Music discovery finds **track folders containing multi-WAV files**:

```
src/assets/audio/radio/americana/gold/
  americana_01_headlights_at_dusk/
    Rusted Sign Glow.1.wav         ← First variant (picks this one)
    Rusted Sign Glow.2.wav
    americana_01_headlights_at_dusk.md
  americana_02_the_two_lane/
    Porch-Fire Embers.1.wav
    Porch-Fire Embers.2.wav
    ...
```

Discovery extracts:
- **Directory name** → `output_basename`
- **Genre** → from parent folder name
- **First WAV file** → `input_file` path

### Discovery Commands

```bash
# Discover all 11 music genres
cd tools/tts && make music-discover-gold

# Single genre
make music-discover-gold-americana    # Americana only
make music-discover-gold-bluespunk
make music-discover-gold-dreampop
make music-discover-gold-folk
make music-discover-gold-gospel
make music-discover-gold-grunge
make music-discover-gold-jazz
make music-discover-gold-nordic
make music-discover-gold-pagan
make music-discover-gold-vaporwave
make music-discover-gold-drone
```

Direct script invocation:

```bash
./tools/tts/discover_music_gold.sh                       # All 11 genres
./tools/tts/discover_music_gold.sh --genre americana     # Americana only
./tools/tts/discover_music_gold.sh --genre jazz          # Jazz only
```

### Generated Manifest Format

```csv
output_basename,genre,input_file
"americana_01_headlights_at_dusk","americana","americana/gold/americana_01_headlights_at_dusk/Rusted Sign Glow.1.wav"
"americana_02_the_two_lane","americana","americana/gold/americana_02_the_two_lane/Porch-Fire Embers.2.wav"
```

Fields derived from:
- `output_basename` → track folder name (e.g., `americana_01_headlights_at_dusk`)
- `genre` → parent folder name (e.g., `americana`)
- `input_file` → first WAV file found in track folder (relative path from audio root)

### Discovered Manifest Locations

```
tools/tts/scripts/
  americana_discovered.csv    (20 tracks — complete)
  nordic_discovered.csv       (6 tracks)
  bluespunk_discovered.csv    (4 tracks)
  drone_discovered.csv        (4 tracks)
  dreampop_discovered.csv     (3 tracks)
  folk_discovered.csv         (3 tracks)
  gospel_discovered.csv       (3 tracks)
  grunge_discovered.csv       (3 tracks)
  pagan_discovered.csv        (3 tracks)
  vaporwave_discovered.csv    (3 tracks)
  jazz_discovered.csv         (2 tracks)
```

### Complete Music Workflow (Discover → Batch → Install)

```bash
# 1. Discover gold masters
cd tools/tts && make music-discover-gold-americana

# 2. Batch process discovered tracks
make music-batch-americana

# 3. Install to submodule
make install-music-americana

# Or one-liner
make music-discover-gold-americana && make music-batch-americana
```

Process all genres at once:

```bash
make music-discover-gold          # Discover all 11 genres
make music-batch-all              # Batch all genres sequentially
make install-music-all            # Install everything to submodule
```

### Legacy Gold-Folder Autodiscovery Rules

`tools/music/` (planned tooling) also supports drop-in autodiscovery: drop any WAV file into a station's gold directory and `make batch-process` will pick it up automatically.

Rules:
- Files must match the naming convention: `{station}_track_NN_{descriptor}.wav`
- Files must be 48 kHz/24-bit mono or stereo
- `manifest.json` is auto-updated on next `make music-manifests` run

Discovery logic lives in `tools/music/scripts/discover_gold.sh`:

```bash
# Scans all tools/music/gold/*/ directories
# Lists WAVs not yet in their station's manifest
# Outputs a summary of new tracks to process
```

## Per-Station EQ Profiles — Full Spec

Each of the 11 music stations has a **distinct sonic character** defined by genre-specific EQ profiles. Processing is automatic based on genre selection (not per-track configuration). Each track gets **two processed variants**: `clean` (full fidelity, used in good biomes) and `degraded` (signal loss simulation, used in deep biomes).

The per-station EQ defines frequency shaping, presence boost, echo/reverb, and tempo shift. These are tuned so each genre **survives the in-engine Godot radio filter** recognizably.

### Station Profile Table

| Station | Freq | Genre | Clean (key EQ) | Degraded (if applicable) | Mood |
|---|---|---|---|---|---|
| **KOLD** 88.1 | Dark Folk | Lowpass 8 kHz (preserve gravelly vocals) | HP 250, LP 5 k, 400 ms echo (wooden room), −5% tempo | Occult, ritualistic, desolate — minimal high-end, heavy reverb |
| **KHRN** 90.1 | Americana | Lowpass 10 kHz, boost at 800 Hz (vocal warmth) | HP 300, LP 4 k, 600 ms echo, −7% tempo | Intimate, wistful, conversational |
| **KFRO** 92.1 | Nordic | Lowpass 9 kHz (sparse folk character) | HP 250, LP 5.5 k, 500 ms echo, −5% tempo | Cold, experimental, minimalist — ancient and otherworldly |
| **KDRM** 94.7 | Dream Pop | Lowpass 14 kHz (preserve shimmer), q:a=7 | HP 300, LP 8 k, 800 ms echo, −10% tempo | Ethereal, oceanic, weightless |
| **KDRN** 95.9 | Drone | Lowpass 10 kHz (preserve long sustain) | HP 300, LP 5 k, 800 ms echo (cathedral), −8% tempo | Minimal, ambient, vast silence — only texture matters |
| **KPGN** 97.3 | Pagan Folk | Boost at 1 kHz (drums + vocals balanced), LP 9 k | HP 300, LP 5.5 k, 500 ms echo (ritualistic), −7% tempo | Hypnotic, ceremonial, rhythmic — trance-like repetition |
| **KJAZ** 98.5 | Jazz | Lowpass 12 kHz (preserve brush detail) | HP 150, LP 6 k, 400 ms echo, −4% tempo | Smoky, melancholic, wide dynamic range — midnight lounge |
| **KVPR** 100.5 | Vaporwave | Lowpass 13 kHz, q:a=7 (preserve synth leads) | **None** (one variant only) | Retro-futuristic, liminal, artificial warmth — no degraded needed, the lo-fi aesthetic is intentional |
| **KGSP** 102.1 | Dark Gospel | Boost at 1.2 kHz (organ/piano presence), LP 11 k | HP 150, LP 6 k, 700 ms echo (cathedral), −8% tempo | Gut-punching, soulful, reverent — power lives in midrange |
| **KGNG** 105.9 | Garage-Psych | HP 60 (preserve punch), boost at 2 kHz, LP 12 k | HP 150, LP 7 k, 300 ms echo (room), −2% tempo (keep energy) | Defiant, cathartic, anthemic — attitude over polish |
| **KBPK** 107.3 | Blues-Punk | HP 40 (punch), boost at 150 Hz (kick drum), LP 13 k | HP 200, LP 7 k, 250 ms echo (quick slap-back), −6% tempo | Defiant, reckless, sweaty — rawness survives compression |

### Two-Variant Model: Clean + Degraded

Each track gets two processed versions:

1. **Clean** (full fidelity)
   - Preserves the genre's high-frequency detail
   - Used in good biomes (Verdant, early Fungal)

2. **Degraded** (signal loss simulation)
   - Narrow EQ, echo, tempo slow
   - Simulates radio signal degradation in deep biomes
   - Tests whether genre survives in-engine radio filter

### Why This Matters — Design Rationale

**Emotional consistency**: each station has a consistent sonic personality. Every Americana track sounds like it belongs on KHRN because they all share the same EQ treatment. The genre-specific EQ is what makes it feel like a real radio station, not a playlist.

**Survival through the radio filter**: in-engine, the Godot radio filter applies additional EQ rolloff. Clean and degraded variants are tuned so each genre **survives this recognizably**:
- **Dream Pop**: preserves high-frequency shimmer (`q:a=7`) in clean because the filter will roll it off
- **Gospel**: boosts midrange because low-end punch gets lost in the filter
- **Vaporwave**: doesn't need degradation because the filter actually enhances the lo-fi vibe

**Biome progression**: as the player drives deeper into the forest, stations degrade per [[Disturbance System]]:
- In **Verdant**: clean variant plays (full EQ profile)
- In **Fungal**: degraded variant plays (narrower EQ, echo, slowdown)
- In **Petrifold**: station goes silent

The degradation is **tuned per genre** so each station maintains its identity even when damaged.

### It's Automatic (not per-track)

You don't set mood per track. You just specify genre:

```bash
make music-process-americana INPUT=file.mp3 OUTPUT=track_01
```

The script automatically applies the Americana EQ profile — lowpass 10 kHz, 800 Hz boost, everything. If you process 18 Americana tracks, all 18 get the same EQ treatment. They're a cohesive station.

### CSV Manifests Don't Define Mood

The CSV manifests look like:

```csv
output_basename,genre,input_file
americana_ghost_story_01,americana,suno_downloads/americana_01.mp3
americana_desert_highway_02,americana,suno_downloads/americana_02.mp3
```

The `genre` column is **just a label** that tells the script which EQ profile to use. You can't mix moods per track — all Americana tracks get the Americana profile.

If you wanted different moods (cheerful vs melancholic Americana), you'd need:
- A separate station for each mood (adds radio stations), OR
- Multi-tier processing like Gary's 4-variant pipeline (see [[Narrative/KTLK Gary — Character Case Study]])

### Customizing EQ Profiles

Edit `tools/tts/music_process.sh`:

```bash
# Change the Americana EQ
run_clean_americana() {
    ffmpeg -y -i "$GOLD_MASTER" \
        -af "lowpass=f=11000, equalizer=f=900:width_type=o:width=2:g=3" \  # ← Edit here
        -c:a libvorbis -q:a 6 \
        "$CLEAN_OGG" \
        -loglevel warning
}
```

Changes apply to the **entire genre** for all tracks — not per-track.

### Summary Table

| Aspect | Implementation |
|--------|---|
| **Per-station mood** | ✅ Yes — genre-specific EQ profiles |
| **Per-track mood** | ❌ No — all tracks in a genre get the same EQ |
| **4-tier moods like Gary** | ❌ No — only clean + degraded variants |
| **Configurable per track** | ❌ No — automatic based on genre selection |
| **Customizable** | ✅ Edit `music_process.sh` to change EQ profiles |

## Per-Genre Makefile Commands

`tools/tts/Makefile` includes music processing targets that run the full pipeline (master → clean → degraded) with genre-specific EQ profiles. Choose the command matching your station:

| Command | Genre | Station | Freq |
|---------|-------|---------|------|
| `make music-process-americana` | Haunted Americana | KHRN | 90.1 |
| `make music-process-bluespunk` | Blues-Punk | KBPK | 107.3 |
| `make music-process-dreampop` | Dream Pop | KDRM | 94.7 |
| `make music-process-folk` | Dark Folk | KOLD | 88.1 |
| `make music-process-gospel` | Dark Gospel | KGSP | 102.1 |
| `make music-process-grunge` | Garage-Psych | KGNG | 105.9 |
| `make music-process-jazz` | Jazz | KJAZ | 98.5 |
| `make music-process-nordic` | Nordic | KFRO | 92.1 |
| `make music-process-pagan` | Pagan Folk | KPGN | 97.3 |
| `make music-process-vaporwave` | Vaporwave | KVPR | 100.5 |
| `make music-process-drone` | Dark Ambient | KDRN | 95.9 |

### Usage

```bash
make music-process-<genre> INPUT=<mp3|wav> OUTPUT=<basename>
```

Example:

```bash
cd tools/tts
make music-process-dreampop INPUT=dreampop_ethereal_track.mp3 OUTPUT=dreampop_underwater_drift_05
```

This runs the full pipeline in sequence:
1. **Master**: MP3 → 48 kHz/24-bit WAV
2. **Clean**: WAV → clean OGG (full fidelity, genre EQ)
3. **Degraded**: WAV → degraded OGG (narrower EQ, echo, tempo shift) — **skipped for vaporwave**

**Output** to `tools/tts/output/`:

```
americana_lost_highway_01.wav          # Gold master (48 kHz/24-bit stereo)
americana_lost_highway_01.ogg          # Clean variant
americana_lost_highway_01_degraded.ogg # Degraded variant
```

### Advanced — Step-by-Step Processing

Process in stages when you want to listen to the master before processing further, or process the same master to multiple output names:

#### 1. Master Only (MP3/WAV → 48 kHz/24-bit WAV)

```bash
make music-master GENRE=americana INPUT=suno_track.mp3 OUTPUT=americana_track_01
```

**Output**: `tools/tts/output/americana_track_01.wav` (gold master)

#### 2. Clean Only (Master → Clean OGG)

```bash
make music-clean GENRE=americana OUTPUT=americana_track_01
```

**Output**: `tools/tts/output/americana_track_01.ogg`

**Requires**: gold master WAV already exists at `tools/tts/output/americana_track_01.wav`

#### 3. Degraded Only (Master → Degraded OGG)

```bash
make music-degraded GENRE=americana OUTPUT=americana_track_01
```

**Output**: `tools/tts/output/americana_track_01_degraded.ogg`

**Requires**: gold master WAV already exists.

#### 4. Full Pipeline (All Steps)

```bash
make music-process-all GENRE=americana INPUT=suno_track.mp3 OUTPUT=americana_track_01
```

Equivalent to running all three steps sequentially:

```bash
make music-master GENRE=americana INPUT=suno_track.mp3 OUTPUT=americana_track_01
make music-clean GENRE=americana OUTPUT=americana_track_01
make music-degraded GENRE=americana OUTPUT=americana_track_01  # skipped for vaporwave
```

### Workflow Example — Americana

```bash
cd tools/tts

# 1. Export 3 tracks from Suno as MP3
# Files: americana_1.mp3, americana_2.mp3, americana_3.mp3

# 2. Process all three in sequence
make music-process-americana INPUT=americana_1.mp3 OUTPUT=americana_ghost_story_01
make music-process-americana INPUT=americana_2.mp3 OUTPUT=americana_desert_highway_02
make music-process-americana INPUT=americana_3.mp3 OUTPUT=americana_twilight_grief_03

# 3. Check output directory
ls -lh output/ | grep americana
# Expected:
#   americana_ghost_story_01.wav
#   americana_ghost_story_01.ogg
#   americana_ghost_story_01_degraded.ogg
#   americana_desert_highway_02.wav
#   americana_desert_highway_02.ogg
#   americana_desert_highway_02_degraded.ogg
#   ...

# 4. QA listen to the clean OGGs before installing
# Play: americana_ghost_story_01.ogg, etc.

# 5. Move to audio submodule (after approval)
cp output/americana_*.ogg ../src/assets/audio/radio/americana/
cp output/americana_*.wav ../src/assets/audio/radio/americana/gold/

# 6. Commit to submodule and parent repo (two-repo workflow)
cd ../src/assets/audio
git add radio/americana/
git commit -m "feat: add 3 Americana tracks (KHRN 90.1)"
git push

cd ../../..
git add src/assets/audio
git commit -m "chore: bump audio submodule with new Americana tracks"
git push
```

### Vaporwave Special Case

Vaporwave doesn't need a degraded variant. The in-engine radio filter actually *enhances* the vaporwave aesthetic (makes it sound more retro/lo-fi).

```bash
make music-process-vaporwave INPUT=vaporwave_track.mp3 OUTPUT=vaporwave_mall_lobby_03
```

**Output**:

```
tools/tts/output/
  vaporwave_mall_lobby_03.wav  # Gold master
  vaporwave_mall_lobby_03.ogg  # Clean variant only
  # No _degraded variant
```

### Customizing Output Directory

By default output goes to `tools/tts/output/`. Override with `OUTPUT_DIR`:

```bash
make music-process-americana \
  INPUT=suno_track.mp3 \
  OUTPUT=americana_01 \
  OUTPUT_DIR=/tmp/audio_work
```

**Output**: `/tmp/audio_work/americana_01.ogg`

### Batch Processing (shell loop)

For processing many tracks at once, create a shell loop:

```bash
cd tools/tts

# Process 5 Americana tracks in sequence (numbered)
for i in 1 2 3 4 5; do
  make music-process-americana \
    INPUT="suno_downloads/americana_${i}.mp3" \
    OUTPUT="americana_track_$(printf "%02d" $i)"
done

# Or with descriptive names (recommended)
make music-process-americana INPUT=suno_downloads/americana_1.mp3 OUTPUT=americana_ghost_story_01
make music-process-americana INPUT=suno_downloads/americana_2.mp3 OUTPUT=americana_desert_highway_02
make music-process-americana INPUT=suno_downloads/americana_3.mp3 OUTPUT=americana_lost_passenger_03
```

## The 13-Station Roster (Full)

| # | Freq | Station ID | Genre | Channel Dir | Status |
|---|------|-----------|-------|------------|--------|
| 1 | 88.1 | KOLD | Dark Folk / Neofolk | `folk/` | Recharacterized |
| 2 | 90.1 | KHRN | Haunted Americana | `americana/` | New |
| 3 | 92.1 | KFRO | Nordic Experimental | `nordic/` | Retained |
| 4 | 93.3 | KTLK | Talk Radio (Gary) | `talk/` | Unchanged — TTS, not music |
| 5 | 94.7 | KDRM | Dream Pop | `dreampop/` | New |
| 6 | 95.9 | KDRN | Dark Ambient / Drone | `drone/` | Retained |
| 7 | 97.3 | KPGN | Pagan Folk | `pagan/` | New |
| 8 | 98.5 | KJAZ | Jazz | `jazz/` | Retained |
| 9 | 100.5 | KVPR | Vaporwave | `vaporwave/` | New |
| 10 | 102.1 | KGSP | Dark Gospel | `gospel/` | New |
| 11 | 103.7 | KNWS | News | `news/` | Unchanged — TTS, not music |
| 12 | 105.9 | KGNG | PNW Garage-Psych | `grunge/` | Recharacterized |
| 13 | 107.3 | KBPK | Blues-Punk | `bluespunk/` | New |

## Detailed Per-Genre Processing

This section documents the exact ffmpeg filter chains and design rationale for each genre. Automated via the `make music-process-<genre>` commands above.

### Dark Folk / Neofolk (KOLD 88.1)

**Clean variant** (full fidelity):

```bash
ffmpeg -i folk_01.wav \
  -af "lowpass=f=8000" \
  -c:a libvorbis -q:a 6 \
  folk_01.ogg
```

- **Lowpass at 8 kHz**: removes harsh aliasing from Suno's synth artifacts
- **Quality 6**: ~256 kbps (midrange quality for lo-fi aesthetic)
- **Goal**: preserve gravelly vocals, sparse instrumentation, reverb character

**Degraded variant** (biome/transmission distortion):

```bash
ffmpeg -i folk_01.wav \
  -af "highpass=f=250, lowpass=f=5000, aecho=0.8:0.9:400:0.3, atempo=0.95" \
  -c:a libvorbis -q:a 4 \
  folk_01_degraded.ogg
```

- **Highpass at 250 Hz**: remove sub-bass (AM radio ceiling)
- **Lowpass at 5 kHz**: narrow midrange, remove high details
- **Echo at 400 ms**: wooden room reverb (fits the genre)
- **Tempo −5%**: slow, hypnotic quality enhanced
- **Quality 4**: ~160 kbps

**Rationale**: dark folk survives degradation well because its identity lives in vocals and guitar. High-frequency sparkle isn't critical.

### Haunted Americana (KHRN 90.1)

**Clean variant**:

```bash
ffmpeg -i americana_01.wav \
  -af "lowpass=f=10000, equalizer=f=800:width_type=o:width=2:g=2" \
  -c:a libvorbis -q:a 6 \
  americana_01.ogg
```

- **Lowpass at 10 kHz**: slightly higher than folk (more open upper midrange)
- **Boost at 800 Hz**: vocal warmth, guitar midrange presence
- **Quality 6**: ~256 kbps

**Degraded variant**:

```bash
ffmpeg -i americana_01.wav \
  -af "highpass=f=300, lowpass=f=4000, aecho=0.7:0.8:600:0.4, atempo=0.93" \
  -c:a libvorbis -q:a 4 \
  americana_01_degraded.ogg
```

- **Echo at 600 ms**: longer than folk, simulates echo in a canyon/highway
- **Tempo −7%**: more pronounced slowdown (adds to wistful character)

**Rationale**: Americana is vocal-forward. Keep enough low-mid presence for vulnerability even when degraded.

### Nordic Experimental (KFRO 92.1) — *Retained*

Keep existing processing from 047-car-radio phase. Do not reprocess unless updating for 054 expansion.

### Dream Pop (KDRM 94.7)

**Clean variant** — CRITICAL: preserve high-frequency shimmer:

```bash
ffmpeg -i dreampop_05.wav \
  -af "lowpass=f=14000" \
  -c:a libvorbis -q:a 7 \
  dreampop_05.ogg
```

- **Lowpass at 14 kHz**: very open, preserve the shimmering synth/reverb layer
- **Quality 7**: ~320 kbps (near-lossless for dream pop's delicate frequencies)
- **No EQ boost**: let the ethereal character breathe

**Degraded variant** — designed to survive radio filter gracefully:

```bash
ffmpeg -i dreampop_05.wav \
  -af "highpass=f=300, lowpass=f=8000, aecho=0.6:0.7:800:0.25, atempo=0.90" \
  -c:a libvorbis -q:a 5 \
  dreampop_05_degraded.ogg
```

- **Lowpass at 8 kHz**: aggressive high-cut (simulates in-engine radio filter)
- **Echo at 800 ms**: longer, dreamy reverb tail (supports the spacey aesthetic even when compressed)
- **Quality 5**: ~192 kbps

**Rationale**: dream pop is most at risk from the radio filter. The degraded variant tests how well the genre survives frequency rolloff. The long echo acts as a lifeline — even without shimmer, the reverb tail preserves the dreamy quality.

### Vaporwave (KVPR 100.5)

**Clean variant**:

```bash
ffmpeg -i vaporwave_03.wav \
  -af "lowpass=f=13000" \
  -c:a libvorbis -q:a 7 \
  vaporwave_03.ogg
```

- **Lowpass at 13 kHz**: preserve bright synth leads and drum machine sparkle
- **Quality 7**: ~320 kbps
- **Rationale**: vaporwave's "uncanny nostalgia" depends on bright digital details. Don't lose them in clean variant.

**No degraded variant**: vaporwave inherently has a lo-fi, retro aesthetic. The in-engine radio filter actually *enhances* the vaporwave vibe. One variant is sufficient.

**Optional alternative** if degraded variant is desired — keep minimal:

```bash
ffmpeg -i vaporwave_03.wav \
  -af "lowpass=f=10000, atempo=0.97" \
  -c:a libvorbis -q:a 6 \
  vaporwave_03_degraded.ogg
```

### Dark Gospel / Sacred Americana (KGSP 102.1)

**Clean variant**:

```bash
ffmpeg -i gospel_02.wav \
  -af "lowpass=f=11000, equalizer=f=1200:width_type=o:width=2:g=3" \
  -c:a libvorbis -q:a 6 \
  gospel_02.ogg
```

- **Boost at 1.2 kHz**: piano/organ presence, vocal warmth
- **Quality 6**: ~256 kbps
- **Rationale**: gospel's power is in the voice and percussion. Keep warm, present midrange.

**Degraded variant**:

```bash
ffmpeg -i gospel_02.wav \
  -af "highpass=f=150, lowpass=f=6000, aecho=0.8:0.9:700:0.35, atempo=0.92" \
  -c:a libvorbis -q:a 4 \
  gospel_02_degraded.ogg
```

- **Echo at 700 ms**: church/cathedral reverb (fits the spiritual character)
- **Tempo −8%**: slow, reverent

**Rationale**: gospel vocals and percussion survive radio filter well (midrange-heavy). Use degradation to add dimension rather than reduce intelligibility.

### PNW Garage-Psych (KGNG 105.9) — *Recharacterized from Grunge*

**Clean variant**:

```bash
ffmpeg -i grunge_07.wav \
  -af "highpass=f=60, lowpass=f=12000, equalizer=f=2000:width_type=o:width=2:g=2" \
  -c:a libvorbis -q:a 6 \
  grunge_07.ogg
```

- **Highpass at 60 Hz**: remove rumble, keep the fuzz guitar's attack
- **Boost at 2 kHz**: guitar presence, shouted vocals cut through
- **Quality 6**: ~256 kbps
- **Rationale**: garage-psych is loud and present. Don't over-compress it in clean variant.

**Degraded variant**:

```bash
ffmpeg -i grunge_07.wav \
  -af "highpass=f=150, lowpass=f=7000, aecho=0.6:0.7:300:0.2, atempo=0.98" \
  -c:a libvorbis -q:a 5 \
  grunge_07_degraded.ogg
```

- **Short echo at 300 ms**: simulate small room reflection (garage/basement feel)
- **Minimal tempo change**: −2% (energy is the point, don't slow it too much)
- **Quality 5**: ~192 kbps

**Rationale**: garage-psych's fuzz and aggression survive degradation. Even at reduced bitrate, it's still recognizable.

### Blues-Punk (KBPK 107.3)

**Clean variant**:

```bash
ffmpeg -i bluespunk_04.wav \
  -af "highpass=f=40, lowpass=f=13000, equalizer=f=150:width_type=o:width=2:g=3" \
  -c:a libvorbis -q:a 6 \
  bluespunk_04.ogg
```

- **Highpass at 40 Hz**: remove sub-bass rumble (preserve punch)
- **Boost at 150 Hz**: kick drum thump, bass guitar grit
- **Quality 6**: ~256 kbps
- **Rationale**: blues-punk is rhythm and grit. Boost the low-mids for impact.

**Degraded variant**:

```bash
ffmpeg -i bluespunk_04.wav \
  -af "highpass=f=200, lowpass=f=7000, aecho=0.7:0.8:250:0.25, atempo=0.94" \
  -c:a libvorbis -q:a 4 \
  bluespunk_04_degraded.ogg
```

- **Echo at 250 ms**: quick slap-back (enhances the punky, bouncy feel)
- **Tempo −6%**: slightly sluggish, drunk, defiant
- **Quality 4**: ~160 kbps

**Rationale**: blues-punk's character is in attitude, not polish. Degradation adds to the rawness.

### Pagan Folk (KPGN 97.3)

**Clean variant**:

```bash
ffmpeg -i pagan_08.wav \
  -af "lowpass=f=9000, equalizer=f=1000:width_type=o:width=2:g=2" \
  -c:a libvorbis -q:a 6 \
  pagan_08.ogg
```

- **Boost at 1 kHz**: vocals and percussion presence (drums are important in pagan folk)
- **Quality 6**: ~256 kbps
- **Rationale**: pagan folk blends folk and world music. Keep vocal warmth and drumming clarity.

**Degraded variant**:

```bash
ffmpeg -i pagan_08.wav \
  -af "highpass=f=300, lowpass=f=5500, aecho=0.8:0.9:500:0.35, atempo=0.93" \
  -c:a libvorbis -q:a 4 \
  pagan_08_degraded.ogg
```

- **Echo at 500 ms**: ritual/ceremonial reverb
- **Tempo −7%**: hypnotic, trance-like

**Rationale**: pagan folk's repetitive, rhythmic character survives compression. Degradation can enhance the ritualistic mood.

### Jazz (KJAZ 98.5) — *Retained*

Keep existing processing from 047 phase. Jazz has wide dynamic range — test that clean variant preserves both quiet brushed drums and punchy brass.

### Drone (KDRN 95.9) — *Retained*

Ambient drone is already processed. Ensure processing preserves long sustain and spatial depth.

## Genre Frequency Coverage Reference

Summary of per-genre frequency shaping for quick lookup:

| Genre | Lowpass (clean) | Highpass (degraded) | Rationale |
|-------|-----------------|-------------------|-----------|
| Dark Folk | 8000 Hz | 250 Hz | Preserve gravelly vocals, remove extremes |
| Americana | 10000 Hz | 300 Hz | Keep vocal warmth, guitar presence |
| Dream Pop | 14000 Hz | 300 Hz | CRITICAL: preserve high shimmer |
| Vaporwave | 13000 Hz | N/A | Keep bright synth leads, don't degrade |
| Gospel | 11000 Hz | 150 Hz | Vocal power, organ/piano, kick drum |
| Garage-Psych | 12000 Hz | 150 Hz | Fuzz guitar and vocals compete (both present) |
| Blues-Punk | 13000 Hz | 200 Hz | Bass and kick punch, guitar presence |
| Pagan Folk | 9000 Hz | 300 Hz | Drums and vocals balanced |

### Echo / Reverb Selection

- **Shorter (250–400 ms)**: garage-psych, blues-punk (room feel, punch)
- **Medium (500–600 ms)**: Americana, gospel, pagan folk (highway/cathedral)
- **Longer (700–800 ms)**: dream pop (ethereal, floaty)
- **None**: vaporwave (algorithm and synth inherently spacious)

### Tempo Adjustment (degraded variants only)

- **−2% (0.98)**: minimal (garage-psych, keep energy)
- **−5% (0.95)**: moderate (folk, blues-punk)
- **−7% (0.93)**: pronounced (Americana, gospel)
- **−8% (0.92)**: deep (pagan folk, ritualistic)
- **None**: vaporwave, dream pop (timing is compositional)

### Bitrate / Quality Selection

Higher quality for genres with frequency-dependent identity:

| Genre | Clean | Degraded | Rationale |
|-------|-------|----------|-----------|
| Dream Pop | q:a=7 | q:a=5 | Preserve shimmer in clean, test survival in degraded |
| Vaporwave | q:a=7 | N/A | Synth details matter |
| Americana | q:a=6 | q:a=4 | Mid-heavy, degrades gracefully |
| Gospel | q:a=6 | q:a=4 | Vocal + percussion, midrange-focused |
| Garage-Psych | q:a=6 | q:a=5 | Fuzz and attitude survive compression |
| Blues-Punk | q:a=6 | q:a=4 | Raw aesthetic suits lower bitrate |
| Dark Folk | q:a=6 | q:a=4 | Lo-fi is intentional |
| Pagan Folk | q:a=6 | q:a=4 | Repetitive, rhythmic, handles compression |

Quality scale reference:
- `q:a=7` — lossless-like (~320 kbps), for high-frequency-dependent genres
- `q:a=6` — high (~256 kbps), most genres in clean variants
- `q:a=5` — medium (~192 kbps), some degraded variants
- `q:a=4` — low (~160 kbps), degraded variants

## Suno Generation Parameters

**Reference prompt per genre**: `src/assets/audio/radio/<genre>/gold/prompts/<genre>_base.md`

Example (Americana):

```
haunted americana, gothic folk, spectral vocals, reverb electric guitar,
fingerpicked acoustic, viola, mournful, slow tempo, intimate recording,
lo-fi warmth, melancholic, twilight mood, sparse arrangement,
confessional, Pacific Northwest
```

**Generation parameters (Suno)**:
- **Model**: Suno v4 (latest)
- **Style**: use the genre base prompt + variations (see `prompts/` directory)
- **Duration**: default (typically 1:30–2:00 minutes)
- **Number of variations**: generate 2–3 per prompt to build a track pool

**Target pool size**: 15–20 full-length tracks per station (to avoid repetition over a 2-hour session).

### MP3 → WAV Gold Master Step

Suno MP3s are 320 kbps high-quality stereo. Resample to the game's canonical audio format (48 kHz/24-bit):

```bash
ffmpeg -i americana_01.mp3 \
  -ar 48000 \
  -c:a pcm_s24le \
  -ac 2 \
  americana_01.wav
```

**Output**: `americana_01.wav` (48 kHz/24-bit stereo).

**Note**: music is **stereo** (unlike TTS which is mono). The radio filter handles mono-conversion in-engine.

### Listen & QA Before Processing

Before processing further, evaluate each track:

1. **Genre match**: does it sound like the genre guide description? Listen for:
   - Instrumentation (guitars, synths, drums per genre spec)
   - Vocal delivery (ethereal for dream pop, gravelly for dark folk, etc.)
   - Tempo and rhythm (glacial for dark folk, driving for garage-psych, etc.)
   - Production character (lo-fi for folk, polished for dream pop, retro for vaporwave)

2. **Radio filter compatibility**: imagine the in-engine radio filter (lo-fi EQ, narrow stereo, harmonic saturation). Will the genre survive it?
   - **High-risk genres**: Dream Pop, Vaporwave (high-frequency dependent) — test with headphones focusing on high-end shimmer
   - **Low-risk genres**: Blues-Punk, Dark Gospel (midrange-heavy) — should sound good regardless

3. **Track length**: aim for 1:30–2:30 minutes per track (natural song length).

4. **Unusable tracks**: flag for regeneration if:
   - Artifact glitches (AI dropouts, pitch warps)
   - Genre mismatch (generated rock instead of dream pop)
   - Too loud/distorted going in (bad starting point for processing)

## File Organization

### Working Directory (local)

```
~/music_processing/
  suno_downloads/
    americana_01.mp3
    americana_02.mp3
    ...
  converted_wav/
    americana_01.wav
    americana_02.wav
    ...
  processed/
    americana_01.ogg
    americana_01_degraded.ogg
    ...
```

### Delivery to Submodule

Once approved, move processed OGGs to the audio submodule:

```
src/assets/audio/radio/
  americana/
    americana_01.ogg
    americana_01_degraded.ogg
    americana_02.ogg
    americana_02_degraded.ogg
    gold/
      americana_01.wav
      americana_01.json  # provenance (if from Suno/AI)
```

**Gold masters**: if tracks come from Suno, store the highest-quality export (WAV or lossless) in `gold/` with a corresponding `.json` provenance file (optional for music, required for TTS).

## Final Naming Convention

Game-ready OGGs follow the pattern established by TTS clips, but for **music tracks** there's no sequential naming requirement. Use **descriptive names** based on mood or content when possible.

### Option A — Sequential (simple)

```
folk_01.ogg
folk_02.ogg
americana_01.ogg
americana_02.ogg
```

### Option B — Descriptive (recommended for mood-based randomization)

```
folk_midnight_whisper.ogg
folk_winter_ritual.ogg
americana_desert_highway.ogg
americana_ghost_story.ogg
```

### Naming Requirements

- Start with the **genre/channel name** (matches the folder it goes in)
- Use lowercase, underscores between words
- Include `_degraded` suffix for degraded variants
- Ensure filename is **unique within that genre** (avoid duplicates)

**Good examples**:
- `americana_lost_highway.ogg`
- `americana_lost_highway_degraded.ogg`
- `dreampop_underwater_drift.ogg`
- `gospel_hymn_morning.ogg`
- `vaporwave_mall_lobby.ogg`

**Avoid**:
- `suno_001.ogg` (too generic, lose context)
- `Americana Track 5.ogg` (spaces, mixed case)
- `americana-lost-highway.ogg` (use underscore, not dash)

## Quality Control & Testing

### Pre-Processing QA

1. **Listen in context**: play the Suno MP3 within a mix of existing stations (use playlist/loop).
2. **Check genre identity**: can you identify it within 10 seconds without seeing the filename?
3. **Evaluate for artifacts**: any glitches, dropouts, or pitch anomalies from AI?
4. **Tempo consistency**: does it feel natural or have timing issues?

### Post-Processing QA

1. **A/B compare**: play clean vs original side-by-side. Should be similar in character, not drastically different.
2. **Degraded variant test**: listen to the degraded variant with the in-engine radio filter applied (mentally or actually test in-engine):
   - For dream pop: does high-frequency shimmer survive?
   - For vaporwave: does retro synth warmth come through?
   - For blues-punk: is the punch still there?
3. **Bitrate assessment**: at the chosen quality level, are there audible compression artifacts (metallic whine, pumping)? If yes, increase quality (higher `q:a` value) and reprocess.
4. **File size check**: verify OGGs are reasonable size for streaming in-game:
   - Dream pop q:a=7 might be 15–20 MB for a 2-minute track
   - Blues-punk q:a=4 might be 8–12 MB
   - If too large, reduce quality slightly and re-evaluate.

### In-Engine Testing

1. Add tracks to radio channels in Godot (`RadioChannel` resource).
2. Play multiple stations in different biomes.
3. Confirm:
   - Tracks loop seamlessly
   - No dropouts or stuttering
   - Signal degradation (biome-based) sounds intentional, not broken
   - Track order is randomized (not always the same first track)

## Delivery Checklist

Before committing music tracks to the audio submodule:

- [ ] **Gold master WAV** exists in `<genre>/gold/` (48 kHz/24-bit stereo)
- [ ] **Provenance JSON** exists in `<genre>/gold/` (if from Suno/AI — document model, prompt, generation params)
- [ ] **Clean OGG** exists in `<genre>/` (matches base filename of WAV)
- [ ] **Degraded OGG** exists in `<genre>/` (if applicable — includes `_degraded` suffix)
- [ ] **QA checklist** completed (genre identity, artifact check, bitrate validation)
- [ ] **Filenames** follow convention (lowercase, underscores, unique within genre)
- [ ] **No duplicate filenames** with existing tracks in that genre
- [ ] **File sizes** reasonable (≤25 MB per 2-minute track at chosen bitrate)
- [ ] **In-engine test** passed (radio plays track, no dropouts, signal degradation works)

## Troubleshooting

### "Dream pop sounds like mush after processing"

- Check lowpass frequency. May be too aggressive (drop it below 10 kHz and you lose the shimmer that makes it dream pop).
- In clean variant: use `q:a=7` or even lossless (FLAC for testing).
- Verify Suno MP3 actually has high-frequency detail (listen to it unprocessed first).

### "Vaporwave lost its character after the radio filter"

- Vaporwave is designed to survive lo-fi. If it doesn't, the Suno generation may have lacked bright synth leads to begin with.
- Re-generate with prompt emphasizing "bright", "shimmering", "neon", "crisp".
- Don't over-process — minimal EQ, high bitrate.

### "Garage-psych is too loud / clipping"

- Suno sometimes outputs hot. Normalize or add a gain reduction filter before lowpass/EQ:

```bash
-af "loudnorm=I=-23:TP=-1.5:LRA=7,lowpass=f=12000,..."
```

### "Music is distorted / artifacts appearing"

- MP3 artifact from Suno → WAV conversion. Process at lower quality (`q:a=4`) to see if bitrate reduction helps mask it.
- If artifacts are in the Suno MP3 itself, regenerate the track.

### "Degraded variant doesn't sound different enough"

- Increase the lowpass cutoff reduction (e.g., 5000 Hz instead of 6000 Hz).
- Add longer echo (e.g., 800 ms instead of 500 ms).
- Increase tempo reduction (e.g., 0.90 instead of 0.96).
- Verify the clean variant isn't already compressed too much.

### Command Not Found / Missing Parameters

**"Command not found: music_process.sh"** — ensure you're in `tools/tts/`:

```bash
cd /path/to/oldgrowth/tools/tts
make music-process-americana INPUT=... OUTPUT=...
```

**"ERROR: GENRE is required"** — pass the genre explicitly or use the per-genre shortcut:

```bash
# Wrong:
make music-process-all INPUT=track.mp3 OUTPUT=track_01  # Missing GENRE

# Right:
make music-process-all GENRE=americana INPUT=track.mp3 OUTPUT=track_01
# OR
make music-process-americana INPUT=track.mp3 OUTPUT=track_01
```

**"ERROR: INPUT (mp3|wav file) is required"** — you're using a target that needs an input file (master or all). Either provide `INPUT` with the filename, or use a clean/degraded target that works from an existing master.

**"ffmpeg: not found"** — install ffmpeg:

```bash
# macOS
brew install ffmpeg

# Ubuntu
sudo apt-get install ffmpeg
```

## Frequently Asked Questions

**Q: Do I need to generate degraded variants for every track?**
A: No. Vaporwave works best with just one variant (the in-engine radio filter is enough). Other genres benefit from explicit degraded variants to add dimension. Minimum: clean OGGs for all stations. Ideal: clean + degraded for narrative / biome differentiation.

**Q: Can I use Suno's "continue song" feature to make longer tracks?**
A: Yes. Suno lets you extend songs for continuity. Aim for 1:30–2:30 total per track (natural song length). Over 3 minutes risks repetition and file size bloat.

**Q: What if I want to manually create music instead of using Suno?**
A: Music can come from any source (recorded by hand, MuseNet, other AI, commissioned artists). Process the same way: export WAV (48 kHz/24-bit), apply genre-specific EQ, create degraded variant, name per convention. Store gold masters with provenance.

**Q: How many tracks do I need per station to avoid repetition?**
A: Aim for 15–20 full-length tracks per station. A 2-hour play session at 2 minutes per track = 60 tracks heard. With 15–20 per station, the player will hear repeats but not obviously (especially with randomization and biome-based selection).

**Q: Can I edit Suno tracks post-generation (e.g., remove a verse)?**
A: Yes. Use ffmpeg's `atrim` filter:

```bash
ffmpeg -i track.wav -af "atrim=start=0:end=90" track_trimmed.wav
```

Trim to meaningful sections (verses/choruses) but keep natural endpoints.

**Q: Do I need to provide sheet music or lyrics?**
A: No. Music is stored as audio only (OGG for game, WAV for archive). If you want to document track content (lyrics, instrumentation, Suno prompt), store that as `.md` in `<genre>/gold/prompts/` for reference.

**Q: Output OGG sounds worse than input WAV — what's wrong?**
A: Check the genre's quality setting. Quality levels are: `q:a=7` lossless-like (~320 kbps, for dream pop/vaporwave), `q:a=6` high (~256 kbps, most genres clean), `q:a=5` medium (~192 kbps), `q:a=4` low (~160 kbps, degraded variants). If quality is too low, edit `music_process.sh` and increase `q:a` for that genre.

## Music Batch Processing with CSV Manifests

An alternative to per-track `make music-process-<genre>` calls: the batch processor reads a CSV manifest and processes all listed tracks in sequence. Use this when managing 15–20 tracks per station.

### CSV Manifest Format

Different from TTS manifests — music batch uses 3-column format:

```csv
output_basename,genre,input_file
americana_ghost_story_01,americana,suno_downloads/americana_01.mp3
americana_desert_highway_02,americana,suno_downloads/americana_02.mp3
americana_lost_passenger_03,americana,suno_downloads/americana_3.wav
```

**Columns**:
- `output_basename` — final filename without extension (use descriptive names per mood/theme)
- `genre` — genre code (americana, bluespunk, dreampop, etc.)
- `input_file` — path to source MP3/WAV file

Each genre has a template manifest at `tools/tts/scripts/<genre>_music.csv`.

### Batch Workflow — End to End

#### Step 1 — Generate Music in Suno

For each genre, generate multiple tracks to build a track pool.

**Target**: 15–20 tracks per station (prevents repetition in 2-hour sessions).

1. Go to https://suno.com
2. Use the genre's base prompt + variations from [[Audio — Radio Stations]]
3. Generate 2–3 variations per prompt (Suno offers multiple versions)
4. Download each as MP3
5. Name descriptively: `americana_01.mp3`, `americana_02.mp3`, etc.
6. Store in `suno_downloads/` folder (or your working directory)

```
suno_downloads/
  americana_01.mp3  (prompt: haunted americana, ghost story)
  americana_02.mp3  (prompt: haunted americana, desert highway variant)
  americana_03.mp3  (prompt: haunted americana, lost passenger)
  ...  (up to 15–20 total)
```

#### Step 2 — Prepare the CSV Manifest

Edit the template at `tools/tts/scripts/<genre>_music.csv` to list each track. Use descriptive output basenames per mood or theme.

#### Step 3 — Run Batch Processing

```bash
cd tools/tts
make music-batch-<genre>    # e.g., make music-batch-americana
```

Or invoke the script directly:

```bash
./tools/tts/music_batch.sh \
    --manifest scripts/americana_music.csv \
    --output-dir output/
```

#### Step 4 — QA and Install

Listen to output OGGs, then install approved tracks to the audio submodule:

```bash
# Copy approved tracks to submodule
cp output/americana_*.ogg src/assets/audio/radio/americana/
cp output/americana_*.wav src/assets/audio/radio/americana/gold/

# Two-repo commit workflow (submodule first, then parent bump)
cd src/assets/audio
git add radio/americana/
git commit -m "feat: add N Americana tracks (KHRN 90.1)"
git push

cd ../../..
git add src/assets/audio
git commit -m "chore: bump audio submodule with new Americana tracks"
git push
```

### How `music_batch.sh` Finds WAVs

**TL;DR**: the batch processor doesn't discover WAVs at runtime. It reads a CSV manifest that lists all input files explicitly. The manifest is the complete list — nothing is implicit.

Flow:

```
CSV Manifest (scripts/<genre>_music.csv)
    ↓
music_batch.sh reads it line-by-line
    ↓
For each row:
  output_basename,genre,input_file
  ↓
  Checks if input_file exists
  ↓
  Calls music_process.sh with that file
```

Simplified bash internals:

```bash
while IFS=',' read -r output_basename genre input_file; do
    # Step 1: Skip header and empty rows
    [[ "$output_basename" == "output_basename" ]] && continue
    [[ -z "$output_basename" ]] && continue

    # Step 2: Validate input file exists
    if [[ ! -f "$input_file" ]]; then
        echo "FAIL: Input file not found: $input_file"
        failed=$((failed + 1))
        continue
    fi

    # Step 3: Call music_process.sh with this file
    music_process.sh \
        --genre "$genre" \
        --input "$input_file" \
        --output "$output_basename" \
        --mode all \
        --output-dir output/

done < "$MANIFEST"  # ← Read from CSV manifest file
```

### Two Batch Approaches — CSV vs Auto-Discovery

The music pipeline supports two batch modes:

| Mode | Source | Use Case |
|------|--------|----------|
| **CSV manifest** (this section) | Hand-authored `scripts/<genre>_music.csv` | New track authoring from Suno downloads |
| **Auto-discovery** (earlier section) | `scripts/<genre>_discovered.csv` auto-generated from gold/ | Reprocessing existing archival masters |

Both produce the same output OGGs. Choose based on workflow — CSV manifests give explicit control for new content; auto-discovery is zero-effort for existing gold folder content.

## References by Genre

Each genre has a prompt guide in the audio submodule. Check before generating new tracks.

**Location**: `src/assets/audio/radio/<genre>/gold/prompts/<genre>_base.md`

**Genres with guides**:
- americana (KHRN, new)
- bluespunk (KBPK, new)
- dreampop (KDRM, new)
- folk (KOLD, existing)
- gospel (KGSP, new)
- grunge (KGNG, recharacterized)
- jazz (KJAZ, existing)
- nordic (KFRO, existing)
- pagan (KPGN, new)
- vaporwave (KVPR, new)
- drone (KDRN, existing — dark ambient)

Each guide includes:
- Base prompt for Suno
- Variation prompts (instrumentation focus, mood variants)
- Suno style tag recommendations
- Production notes and reference artists

## Additional Resources

- **Suno**: https://suno.com (AI music generation)
- **ffmpeg documentation**: https://ffmpeg.org/documentation.html
- **Audio Submodule**: https://github.com/shapeandshare/oldgrowth-assets (Git LFS)
- **Genre guide**: [[Audio — Radio Stations]] (detailed sonic descriptions per genre)
- **Radio system design**: spec 047 (original radio implementation)
- **Expansion spec**: spec 054 (11 new stations, frequency map, requirements)
- **TTS pipeline**: [[Audio — Pipeline]] (spoken content pipeline for DJ clips)

## Mood Processing (Optional Enhancement Layer)

Some tracks benefit from an additional "mood" processing pass beyond the station filter chain. Typical moods:

| Mood | Additional ffmpeg filters | Use Case |
|---|---|---|
| `night_drive` | `aecho=0.4:0.5:30:0.15` (subtle reverb) | Late-night warmth |
| `distant` | `lowpass=f=5000` additional (further narrowing) | Track plays "behind" other audio |
| `unsettled` | `vibrato=f=2:d=0.015` (subtle pitch warble) | Degradation hint |
| `storm` | `aecho=0.6:0.7:200:0.4` (long reverb) | Deep Fungal / Petrifold |

Mood processing is **optional** — most tracks ship with just the station filter chain. Mood is applied via:

```bash
make apply-mood STATION=kold MOOD=night_drive TRACK=kold_track_01_forest_isolation
```

Outputs a `_{mood}.ogg` variant alongside the clean OGG. Runtime can select between variants based on biome × disturbance state.

## Batch Processors — Unified

Several pipeline steps share common scaffolding. `tools/music/scripts/batch_processor.sh` provides a unified interface:

```bash
# Process all gold masters to OGG
batch_processor.sh --mode=music --input=tools/music/gold/ --output=tools/music/ogg/

# Apply mood to a set of tracks
batch_processor.sh --mode=mood --station=kold --mood=night_drive

# Install to submodule
batch_processor.sh --mode=install --station=all

# Discover new tracks
batch_processor.sh --mode=discover
```

Unified processor handles:
- Parallel job execution (configurable via `--jobs=N`)
- Progress reporting with counts and timings
- Error collection (per-track failures don't abort the whole batch)
- Dry-run mode for validation (`--dry-run`)
- Resume mode (skips already-processed tracks)

## Provenance Requirements

Every gold master WAV has a co-located `.json` provenance sidecar (same schema as TTS pipeline — see [[Audio — Pipeline]] § Provenance Requirements):

```json
{
  "schema_version": "1.0",
  "generator": "suno",
  "suno_prompt": "dark folk, neofolk, acoustic, reverb-drenched vocals, baritone whisper...",
  "suno_variant_id": "suno-xxx-yyy-zzz",
  "suno_plan_tier": "premier",
  "created_at": "2026-04-21T14:30:00Z",
  "station": "KOLD",
  "frequency": 88.1,
  "genre": "dark_folk",
  "track_title": "Forest Isolation",
  "duration_sec": 184,
  "gold_master": {
    "sha256": "abc123..."
  },
  "commercial_license": {
    "tier": "premier",
    "grants_commercial_rights": true,
    "verified_at": "2026-04-21"
  }
}
```

**CI enforcement**: `make validate-provenance` checks every gold master has provenance; `make validate-commercial-license` verifies `commercial_license.grants_commercial_rights = true` for all music tracks.

## Directory Structure Summary

```
tools/music/                 # Dev-time only; not shipped
├── raw/                     # MP3 downloads from Suno
│   ├── kold/
│   ├── khrn/
│   └── ... (per station)
├── gold/                    # 48kHz/24-bit WAV + provenance JSON
│   ├── kold/
│   │   ├── kold_track_01_forest_isolation.wav
│   │   └── kold_track_01_forest_isolation.json
│   └── ... (per station)
├── ogg/                     # Post-processed OGG ready to ship
│   ├── kold/
│   │   └── kold_track_01_forest_isolation.ogg
│   └── ... (per station)
├── manifests/               # Station-level JSON manifests
│   ├── kold.json
│   └── ... (per station)
├── scripts/                 # Automation
│   ├── discover_gold.sh
│   ├── batch_processor.sh
│   └── ...
└── Makefile                 # All targets

src/assets/audio/radio/music/   # Shipped (submodule, Git LFS)
├── kold/
│   ├── kold_track_01_forest_isolation.ogg
│   ├── manifest.json
│   └── gold/                # .gdignore
│       └── kold_track_01_forest_isolation.{wav,json}
└── ... (per station)
```

## Two-Repo Commit Workflow

Same pattern as TTS pipeline (see [[Audio — Pipeline]]):
1. Music changes commit to submodule first
2. Parent repo updates submodule pointer
3. Separate commits, separate pushes

## Environment Variables

| Var | Default | Notes |
|---|---|---|
| `MUSIC_OUTPUT_DIR` | `tools/music/` | Override working directory |
| `JOBS` | 4 | Parallel processing job count |

## Troubleshooting

### Suno Generation Issues

- **"Commercial rights unclear"** → verify Suno plan is Pro or Premier; Basic does NOT grant commercial use
- **Suno artifacts (abrupt endings, glitches)** → regenerate with refined prompt; reject tracks that can't be fixed
- **Track too short** → Suno sometimes produces tracks shorter than requested; regenerate or extend via Suno's "continue" feature

### Post-Processing Failures

- **"FFmpeg filter chain syntax error"** → check filter chain separator (use commas, not spaces)
- **"Output clipped"** → reduce compression ratio; check `threshold` and `ratio` combination
- **"Output silent"** → verify input WAV is not already silent; check filter chain didn't accidentally cut all signal

### Provenance Validation

- **"Commercial license not verified"** → add `commercial_license.grants_commercial_rights: true` to JSON
- **"Manifest out of date"** → run `make music-manifests` to regenerate

## References

- Spec: `docs/vault/Specs/049 Audio Asset Management/` (provenance schema, pipeline design)
- Spec: `docs/vault/Specs/054 Radio Genre Expansion/` (13-station genre lanes)
- Spec: `docs/vault/Specs/047 Car Radio/` (car radio implementation)
- Suno: https://suno.com/
- Suno commercial rights: https://help.suno.ai/en/articles/8692533-copyright-ownership

## Related

- [[Audio — Pipeline]] — TTS (spoken) pipeline; shares provenance + submodule workflow patterns
- [[Audio — Radio Stations]] — complete per-genre Suno prompt keyword libraries
- [[Audio — Car Radio]] — runtime that plays music content
- [[Tools — TTS]] — TTS-specific implementation walk
- [[Build System]] — CI pipeline
- [[Tooling — Domain MOC]] — domain MOC
- [[Audio — Domain MOC]] — domain MOC
- [[Constitution]] — Article III Rule 13 (Python/Make dev-time-only), Governance Rule 4 (submodule workflow)
ember's answer for note_placement-code-tools-music-pipeline, placementProbability ember gave each option.Art0.06Code0.25 goldDecisions0.04Design0.38 ember's pickDiscoveries0.03Governance0.01Narrative0.01Reference0.06Sessions0.01Systems0.16

note_placement-discoveries-pre-commit-hook-semver-check-blind-spot: placement

Note placement · dev
Gold
Place the note under Discoveries/ (Discoveries)
ember
Place the note under Code/ (Code)
Signal
confidence 0.58 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Discoveries/Pre-Commit Hook SemVer Check Blind Spot.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Pre-Commit Hook SemVer Check Blind Spot

The pre-commit hook's SemVer auto-bump mechanism is unreachable for first-time changes to a component because the underlying check script only examines committed changes (`git diff --name-only BASE...HEAD`), not staged changes. When hook output says "Component SemVer check passed" for a commit that adds new component file changes, it's a false negative — the version wasn't bumped and will fail on the next `make semver` run.

## Background

The pre-commit hook (at `scripts/hooks/pre-commit`) claims to auto-bump component semver and re-stage `component_versions.json` when a commit touches files in a component whose version hasn't been bumped. The hook code at line 45-57 supports this:

```bash
if make semver > /dev/null 2>&1; then
    echo -e "${GREEN}✓ Component SemVer check passed${NC}"
else
    # auto-bump path — never reached for first-time changes
    make semver-bump-patch
    git add src/component_versions.json
fi
```

However, `make semver` delegates to `scripts/ci/check_component_semver.sh`, which at line 52 computes:

```python
CHANGED_FILES="$(git diff --name-only "${BASE_COMMIT}...HEAD")"
```

This three-dot diff compares BASE_COMMIT (typically HEAD^) against HEAD. During a pre-commit hook run, the staged changes haven't been committed yet — they're not in HEAD, so they're invisible to this diff. The hook sees only the changes from prior commits on the branch, not the files about to be committed.

## Consequence

- Developer stages changes to a component (e.g., `interior_camera.gd` in the `bus` component)
- Pre-commit hook runs `make semver` → sees zero component file changes in committed history → prints "✓ Component SemVer check passed"
- Commit goes through without bumping `component_versions.json`
- Next `make semver` run (or CI) examines the new diff against HEAD^ → finds un-bumped component → **fails**
- Developer must manually run `make semver-bump-patch` and amend, or the gap gets caught by CI

The auto-bump path in the hook is dead code for the common case. It only triggers when there were already un-bumped changes in the committed history (e.g., a prior commit was made with `--no-verify`).

## How It Manifests

The hook output is misleading: "Component SemVer check passed" appears for a commit that introduces un-bumped component changes. The developer gets no signal that the bump is missing until a subsequent `make semver` or CI run.

## Fix Paths

Two options to resolve:

**Option A — Include staged changes in the semver check**: Modify `check_component_semver.sh` to also diff staged changes (`git diff --cached --name-only`) and include those files in the candidate set. The hook would then detect that staged files belong to an un-bumped component and trigger the auto-bump path.

**Option B — Fix the hook's detection logic**: Change the pre-commit hook to pass staged-file information to the semver check, rather than relying on the committed-history diff alone.

Either fix would make the documented "auto-bumps component semver" behavior actually work for routine commits.

## References

- `scripts/hooks/pre-commit` (lines 43-57, the auto-bump logic)
- `scripts/ci/check_component_semver.sh` (line 52, the `git diff --name-only BASE...HEAD` call)
- `scripts/ci/bump_component_semver.sh` (line 73, same blind spot pattern)
ember's answer for note_placement-discoveries-pre-commit-hook-semver-check-blind-spot, placementProbability ember gave each option.Art0.01Code0.58 ember's pickDecisions0.03Design0.03Discoveries0.16 goldGovernance0.05Narrative0.02Reference0.04Sessions0.01Systems0.07

note_placement-governance-component-semver-policy: placement

Note placement · test
Gold
Place the note under Governance/ (Governance)
ember
Place the note under Code/ (Code)
Signal
confidence 0.37 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Governance/Component SemVer Policy.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Component SemVer Policy

The canonical policy for component semantic versioning in Old Growth. Every first-party code file under `src/` is mapped to a component with its own SemVer. When files in a component change, the component's version MUST bump. CI (`make semver`) rejects PRs that violate this rule. This note operationalizes constitution Article III Rule 15.

## Overview

Semantic versioning in Old Growth operates at the **component level**, not the project level. A component is a bounded set of file paths under `src/` (e.g., all autoloads, all audio scripts, the terrain subsystem). The mapping and versions live in `src/component_versions.json`. CI enforces that every changed file belongs to a component whose version has been strictly bumped.

The single project-level version (game version) is tracked separately in `src/project.godot` `config/version=` and governed by different rules (see constitution Governance / Commit Strategy).

## Key Rules

### Scope

- Versioned files are first-party code under `src/`
- Vendored plugins under `src/addons/` are **excluded** (they have their own upstream versioning)
- Component definitions and versions live in `src/component_versions.json`

### Strict bump rule

If any file mapped to a component changes in a PR, that component's version MUST be bumped.

| Bump level | When |
|------------|------|
| **major** | Breaking API or contract change |
| **minor** | Backward-compatible feature addition |
| **patch** | Backward-compatible fix or refactor |

CI fails when a changed component does not have a strictly greater version than baseline (the merge-base with `main`).

### Registry format

`src/component_versions.json` structure:

- `schema_version` — integer registry schema version
- `components.<component_name>.version` — `MAJOR.MINOR.PATCH`
- `components.<component_name>.paths` — list of path prefixes mapped to that component

Example:

```json
{
  "schema_version": 1,
  "components": {
    "core-autoloads": {
      "version": "0.1.0",
      "paths": ["src/autoloads/"]
    }
  }
}
```

### Non-overlapping paths

A changed file must map to **exactly one** component. Overlapping paths are an error. Unmapped files are an error.

## Developer Workflow

1. Change files in a component.
2. Bump that component's version in `src/component_versions.json`.
3. Open a PR.
4. CI validates that every changed component has a strictly greater SemVer.

### Auto-bump via pre-commit hook

The pre-commit hook auto-bumps patch versions for components whose files have changed, if the developer forgot. This catches the common case. More significant bumps (minor/major) must be applied manually with justification.

### Local testing

- `make semver` — runs the same SemVer guard used in CI
- `make pre-commit` — runs semver + other fast checks
- `make all` — full CI-like pipeline

## Adding a New Component

1. Add a new `components.<name>` entry in `src/component_versions.json`
2. Set initial SemVer (recommended `0.1.0`)
3. Add one or more non-overlapping path prefixes under `paths`
4. Ensure paths do not point to `src/addons/`

## Common CI Failures

- **Unmapped files**: changed first-party files not covered by any component path
- **Ambiguous mapping**: a changed file matches multiple component paths
- **Missing bump**: component changed but version did not increase
- **Invalid SemVer**: version is not `MAJOR.MINOR.PATCH`

## Related

- [[Constitution]] — Article III Rule 15 is the authoritative source
- [[Build System]] — `make semver` and the CI pipeline
- [[Governance]] — folder MOC
- [[Tech Stack Review]] — what components exist and why
ember's answer for note_placement-governance-component-semver-policy, placementProbability ember gave each option.Art0.01Code0.37 ember's pickDecisions0.11Design0.07Discoveries0.01Governance0.17 goldNarrative0.00Reference0.08Sessions0.00Systems0.18

note_placement-reference-autonomous-company-ngrok-tunnel-setup: placement

Note placement · test
Gold
Place the note under Reference/ (Reference)
ember
Place the note under Systems/ (Systems)
Signal
confidence 0.57 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Reference/Autonomous Company/Ngrok Tunnel Setup.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Ngrok Tunnel Setup

> **Part of**: [[134 Per Persona Slack App Registration]]
> **Purpose**: Provision and maintain the ngrok TCP tunnel that enables GitHub Actions runners to write `releases.shipped` Valkey Stream entries to the local dev machine.

This setup is **one-time provisioning** (Week 0 of the v0.1 experiment). Once configured, the tunnel runs continuously as a macOS launchd service. Required for GitHub Actions → local Valkey reach per [[133 Autonomous Company V01 Experiment]] FR-005a + R-013.

**Prerequisites**:
- Active paid ngrok subscription with reserved-domain capability (per Constitution v2.23.2 Appendix A)
- Valkey 8.0 installed and running locally (per spec 133 Stack 5 Quickstart)
- GitHub repo admin access (for managing Actions secrets)
- macOS developer machine (all-local v0.1 deployment topology)

---

## 1. Reserve a TCP domain in ngrok

1. Visit [dashboard.ngrok.com/cloud-edge/domains](https://dashboard.ngrok.com/cloud-edge/domains)
2. Click **"Reserve a TCP address"**
3. ngrok assigns a reserved TCP endpoint in the format `tcp://<hostname>:<port>`
   - **Actual**: `1.tcp.us-cal-1.ngrok.io:24971`
   - Note: ngrok TCP reservations use numbered hostnames with regional zones
4. Capture the two values separately:
   - **Hostname**: `1.tcp.us-cal-1.ngrok.io`
   - **Port**: `24971`
5. Store both in 1Password as `Ngrok TCP Tunnel — Reserved Endpoint` (entry already created: ID `rd_39lBaWDzYl2EXB4mr2vAe76yEq2`)
6. ⚠️ The port is NOT 6379 on the ngrok side — it is randomly assigned by ngrok at reservation time

---

## 2. Configure Valkey `requirepass`

> **See**: [[134 Per Persona Slack App Registration]] FR-017; spec 133 FR-002a token discipline

Auth happens at the Valkey layer, NOT at the ngrok layer. This keeps the ngrok configuration simple and aligns with the standard Valkey auth model.

1. Generate a strong auth token:
   ```bash
   openssl rand -base64 32
   ```
2. Store the token:
   - **1Password** (canonical) — create entry `Valkey AUTH Token (Local Dev)`
   - **`.env`** (runtime cache) — add `VALKEY_AUTH_TOKEN=<token>`
   - **GitHub repo secrets** (CI) — add `VALKEY_AUTH_TOKEN` (see section 5)
3. Configure Valkey to require the token:
   ```bash
   # Edit valkey.conf (default location: /opt/homebrew/etc/valkey.conf or /usr/local/etc/valkey.conf)
   echo "requirepass <token>" >> /path/to/valkey.conf
   ```
4. Restart Valkey:
   ```bash
   brew services restart valkey
   ```
5. Verify auth is working:
   ```bash
   valkey-cli -a <token> ping
   # Expected: PONG
   ```

---

## 3. Configure ngrok agent

> **See**: [[134 Per Persona Slack App Registration]] FR-016

The ngrok agent runs on the dev machine and forwards inbound TCP from the reserved endpoint to localhost:6379.

1. Create or edit the ngrok config file (default macOS Homebrew path: `/Users/joshburt/Library/Application Support/ngrok/ngrok.yml`):
   ```yaml
   version: "2"
   authtoken: <from 1Password>
   tunnels:
     valkey:
       proto: tcp
       addr: 6379
       remote_addr: 1.tcp.us-cal-1.ngrok.io:24971
   ```
   ⚠️ **Important**: ngrok TCP tunnels use `remote_addr:` with hostname:port. Do NOT add a `domain:` field — that field is for HTTP tunnels only and will cause an invalid config on TCP tunnels.

2. Verify the config parses correctly:
   ```bash
   ngrok start --all --config ~/.config/ngrok/ngrok.yml --dry-run
   ```

---

## 4. Install launchd service for auto-start

The ngrok agent should start automatically at login and restart on crash. This is done via a macOS launchd plist.

1. Create `~/Library/LaunchAgents/com.ngrok.tunnel.plist`:
   ```xml
   <?xml version="1.0" encoding="UTF-8"?>
   <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   <plist version="1.0">
   <dict>
       <key>Label</key>
       <string>com.ngrok.tunnel</string>
       <key>ProgramArguments</key>
       <array>
           <string>/usr/local/bin/ngrok</string>
           <string>start</string>
           <string>--all</string>
           <string>--config</string>
           <string>/Users/joshburt/Library/Application Support/ngrok/ngrok.yml</string>
       </array>
       <key>RunAtLoad</key>
       <true/>
       <key>KeepAlive</key>
       <true/>
       <key>StandardOutPath</key>
       <string>${HOME}/Library/Logs/ngrok.log</string>
       <key>StandardErrorPath</key>
       <string>${HOME}/Library/Logs/ngrok.log</string>
   </dict>
   </plist>
   ```
   Note: Replace `${HOME}` with the actual absolute path (e.g., `/Users/username`). launchd does not expand shell variables.

2. Load the service:
   ```bash
   launchctl load ~/Library/LaunchAgents/com.ngrok.tunnel.plist
   ```

3. Verify it's running:
   ```bash
   launchctl list | grep com.ngrok.tunnel
   # Expected: shows PID and exit code 0
   ```

---

## 5. Store GitHub repo secrets

These secrets are required by the `release.yml` workflow to connect to the Valkey instance via the ngrok tunnel.

| Secret | Value | Source |
|---|---|---|
| `VALKEY_NGROK_HOST` | `1.tcp.us-cal-1.ngrok.io` | ngrok TCP reservation |
| `VALKEY_NGROK_PORT` | `24971` | ngrok TCP reservation |
| `VALKEY_AUTH_TOKEN` | 32-byte base64 token | Generated in section 2 |

Add to GitHub: repository → Settings → Secrets and variables → Actions → New repository secret.

---

## 6. Verify tunnel from external network

This test confirms the tunnel is reachable from outside the dev machine (simulating a GitHub Actions runner):

1. From a non-local machine (e.g., phone hotspot, or another computer):
   ```bash
   valkey-cli -h <reserved>.tcp.ngrok.io -p <port> -a <VALKEY_AUTH_TOKEN> ping
   # Expected: PONG
   ```

2. If the ping fails:
   - Is the ngrok agent running? → `launchctl list | grep com.ngrok.tunnel`
   - Is Valkey running? → `brew services list | grep valkey`
   - Is the requirepass correct? → Check 1Password entry matches GitHub secret
   - Is the ngrok tunnel online? → Check `~/Library/Logs/ngrok.log` for errors

---

## 7. Token rotation

> **See**: spec 133 FR-002a token discipline; FR-017

The `VALKEY_AUTH_TOKEN` must be rotated **monthly** (matching Slack-token rotation cadence per spec 133 FR-002a). Sequence:

1. Generate new token: `openssl rand -base64 32`
2. Update **1Password** entry `Valkey AUTH Token (Local Dev)` — canonical source updated first
3. Update **`.env`** — `VALKEY_AUTH_TOKEN=<new_token>`
4. Update **Valkey config** — replace `requirepass` value in `valkey.conf`
5. Restart Valkey: `brew services restart valkey`
6. Update **GitHub repo secret** — `VALKEY_AUTH_TOKEN`
7. Verify: `valkey-cli -a <new_token> ping` → `PONG`
8. Revoke old token by confirming Valkey is no longer accepting connections with it

---

## 8. Tunnel operations reference

### Start the tunnel
```bash
launchctl load ~/Library/LaunchAgents/com.ngrok.tunnel.plist
```

### Stop the tunnel
```bash
launchctl unload ~/Library/LaunchAgents/com.ngrok.tunnel.plist
```

### Check tunnel status
```bash
launchctl list | grep com.ngrok.tunnel
```

### View tunnel logs
```bash
cat ~/Library/Logs/ngrok.log
```

### Test tunnel connectivity (local)
```bash
valkey-cli -a $VALKEY_AUTH_TOKEN ping
```

### Test tunnel connectivity (external — simulates CI runner)
```bash
valkey-cli -h $VALKEY_NGROK_HOST -p $VALKEY_NGROK_PORT -a $VALKEY_AUTH_TOKEN ping
```

---

## Failure modes

### ngrok agent is down during a release

The `release.yml` workflow's Valkey-write step uses `continue-on-error: true` per FR-019 — the release publication itself succeeds; only the durability enhancement fails. Recovery:

1. Check `~/Library/Logs/ngrok.log` for errors
2. Restart ngrok: `launchctl unload ... && launchctl load ...`
3. Missed `releases.shipped` entries can be recovered via GitHub's webhook redelivery (up to 24h window) OR via the Coordinator's startup-replay mechanism (consumer-group `XPENDING` when Valkey reconnects per spec 133 R-013).
4. Beyond 24h with the tunnel still down, the event is permanently lost — accepted v0.1 risk per [[133 Autonomous Company V01 Experiment]] FR-005a.

### Valkey `requirepass` mismatch

If the GitHub Actions secret `VALKEY_AUTH_TOKEN` does not match Valkey's configured `requirepass`, the XADD step fails with a `NOAUTH` response. The release still publishes; the workflow log shows a clear auth error.

### Reserved domain misconfigured

If `VALKEY_NGROK_HOST` or `VALKEY_NGROK_PORT` are incorrect, the workflow step fails with a connection timeout or DNS resolution error. Verify the values in GitHub repo secrets match the ngrok web UI reservation.

---

## Lifecycle

- **Week 0**: One-time provisioning (sections 1–6)
- **Monthly**: Token rotation (section 7)
- **End of v0.1 experiment**: Stop ngrok agent; retain reserved domain in ngrok account for potential v0.5 re-use (do not release the reservation)

---

> **Next step after tunnel is operational**: Proceed to Stream 3 (Workflow Builder Class-B Approval workflow). See `docs/vault/Specs/134 Per Persona Slack App Registration/134 Per Persona Slack App Registration - quickstart.md` for the full Week 0 procedure.
ember's answer for note_placement-reference-autonomous-company-ngrok-tunnel-setup, placementProbability ember gave each option.Art0.01Code0.13Decisions0.04Design0.05Discoveries0.03Governance0.03Narrative0.03Reference0.09 goldSessions0.03Systems0.57 ember's pick

note_placement-art-pacific-drive-reference: placement

Note placement · test
Gold
Place the note under Art/ (Art)
ember
Place the note under Reference/ (Reference)
Signal
confidence 0.86 (trust band)
Kit outcome
acted on a wrong answer
Why this gold label
Mined from Art/Pacific Drive Reference.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Pacific Drive Reference

The full visual atmosphere and environment design reference for Pacific Drive, assembled as a peer reference for Old Growth's art direction. Pacific Drive is a masterclass in environmental storytelling through *"the twisting of the familiar"* — Creative Director Cassandra Dracott's core design pillar.

Pacific Drive is NOT a direct model for Old Growth, but a useful peer — it shares the Pacific Northwest setting, the single-vehicle horror premise, and the gradient-of-alienation structure Old Growth pursues. This note is the full zone-by-zone catalog for use in image generation, video prompts, and descriptive prose.

## Setting — The Olympic Exclusion Zone

The OEZ encompasses **3,600 square miles** of the Olympic Peninsula in Washington State. Three concentric wards radiate inward, each established during a different era of government experimentation:
- **Outer Zone** (1967)
- **Mid Zone** (1961)
- **Deep Zone** (1955, original test site)

Massive industrial expansion walls separate each ward. The deeper you travel, the longer the land has "marinated in radiation," the more the environment departs from recognizable reality.

**Art style**: stylized realism — not photorealistic but richly atmospheric. Compared to Simon Stålenhag's retro-futuristic paintings, the surrealism of *Twin Peaks*, and the "New Weird" fiction of Jeff VanderMeer's *Annihilation*. Environments "bathed in pastel tones, creating a mood that is both familiar and eerie," with "wonderfully unrealistic lighting" and "a wide array of strong colors for different biomes and different times of day."

Built in Unreal Engine 4 using a Houdini-based procedural terrain pipeline, with Virtual Textures enabling detailed landscape shaders for roads, foliage scattering, and biome blending. Each junction map exceeds **1 km in size**.

## I. The Outer Zone

*Biomes: Damp Forest and Blistering Woods · Sectors E, F, G · Established 1967*

### Weather and atmospheric conditions

The Outer Zone wears the Pacific Northwest's signature weather like a second skin. **Persistent rain** — light to moderate — spatters against windshields with rhythmic regularity. Fog settles in valleys and threads between tree trunks, reducing visibility to a few car-lengths in dense patches. Overcast skies dominate, casting diffused gray-green light through the forest canopy. Cloud cover is thick, low, and unbroken — rarely does direct sunlight penetrate. During **Electromagnetic Squalls**, localized electrical storms crackle through the air with blue-white lightning arcs.

The instability storm's first approach manifests as a **yellow-tinted wall** of atmospheric distortion closing in from the junction edges — the sky shifts to sickly amber, and anomalies spawn aggressively. The second stage follows as a **churning wall of red swirling light** with irregular wavy edges, accompanied by flashes of blue and orange lightning. Air-raid sirens wail as the red front consumes the landscape. Visibility conditions swing between eerie stillness (fog, drizzle, muted light) and violent atmospheric chaos (storms, electrical discharge, falling debris). The weather is never comfortable — even in calm moments, the air feels charged, expectant.

### Terrain and landscape

Dense **temperate rainforest** dominates — towering Douglas firs, western red cedars, and Sitka spruces form a cathedral-like canopy. The understory is lush with ferns, moss-covered logs, abundant undergrowth. Everything glistens with moisture. Ground textures range from soft rain-saturated earth to rocky outcroppings and patches of gravel.

Roads are **cracked two-lane highways** — asphalt seamed with age, edges crumbling into overgrown shoulders. Vegetation reclaims road margins aggressively; grass and ferns push through fractures. Dirt trails branch off into the forest, rutted and muddy.

- **Damp Forest biome**: quintessential PNW — deep greens, moss on every surface, constant wetness, streams cutting through shallow ravines
- **Blistering Woods biome**: same forested character but introduces more electrical anomaly presence — the air itself seems to hum
- **Sierram**: destroyed town in a valley in Junction E1, features a dilapidated highway bridge passing through its center, surrounded by expansive fields of radioactive Hot Dust, with the colossal anomaly "Cappy" dominating the skyline at roughly 15 stories tall

### Structures and architecture

Abandoned infrastructure dots the forest: empty houses with boarded windows, overgrown yards, collapsed porches. **ARDA research trailers** (Anomalous Research Department) rust among the trees — institutional beige-and-green metal boxes trailing cables to nowhere. Gas stations with shattered canopies and dry pumps mark intersections. Emergency cabins with faded signage appear along trails. Derelict vehicles — sedans, trucks, vans from the 1950s–80s — rust in ditches and on road shoulders, some half-consumed by vegetation.

**Observation towers** rise near Sierram, erected post-1973 to monitor the zone's spreading instability. **Sector Stabilizers** — large industrial machinery — are visible in the distance. Power lines sag between weathered wooden poles, cables occasionally sparking.

The structures feel abandoned mid-activity — as if everyone simply vanished, leaving behind detritus of interrupted lives and halted research. Infrastructure is aged but not yet alien; the decay reads as decades of neglect rather than supernatural corruption.

### Lighting and color palette

Dominant palette: **greens, dark browns, slate grays, forest blacks** — the natural PNW spectrum. Lighting consistently **diffused and overcast**, creating soft shadows and muted contrasts. Light filters through the canopy as dappled gray-green. In rain, surfaces reflect with a wet sheen. At dusk and dawn, warm amber tones briefly warm the edges.

**Night is oppressively dark** — without functioning headlights, visibility drops to near-zero. The car's headlights cut weak yellow cones through the murk, catching rain droplets and fog particles. Electrical anomalies introduce **vivid blue-white** accents — Spark Towers throw crackling blue arcs across dark backgrounds. **Violet lights** mark lead panel resource nodes. The stable energy of the Outer Zone glows **yellow/standard** — a warm but cautious hue.

Occasional bioluminescent elements — glowing flowers, anomalous fungi — provide small points of otherworldly color against the overwhelming green-gray.

### Anomalies and surreal elements

The Outer Zone's anomalies feel like reality developing hairline fractures.

- **Tourists** — signature horror. Beige, featureless crash-test-dummy humanoids with smooth faces, visible ball joints at elbows and knees, **dim pulsing red glow** at the chest. Feet root into the ground in a tar-like substance. Only move when unobserved — turn away and they silently close the distance, sometimes appearing in groups of up to 50. Some adopt poses: saluting, arms outstretched.
- **Abductors** — float overhead. UFO-shaped assemblages of car parts (wheels, doors, exhaust pipes, headlights) with traffic-light "eyes," projecting a **blue spotlight** in patrol mode that shifts to **angry red** upon detection. Latch onto the car magnetically and hurl it.
- **Spark Towers** — tall pylons wreathed in vibrant blue electricity, arcing bolts toward anything nearby.
- **Glittering Boulders** — bright shimmering orbs that detonate on proximity.
- **Shakers** — yellow-glowing cracks in the ground that erupt without warning.
- **Potholes** — floating chunks of asphalt and earth suspended in mid-air — gravitational distortion made visible.
- **Honeypots** — abandoned cars in suspiciously perfect condition, loaded with lit road flares, trigger ambushes.
- **Broken Bunnies** — erratic mechanical scrap-metal creatures that hop and latch onto the car — chirp and bounce ahead like corrupted wildlife.

The anomalies are unsettling but comprehensible; the world is wrong, but you can still recognize what it used to be.

### Mood and atmosphere

Developer: *"the feeling of being out there in the woods with this old car, a surreal experience."* Mood oscillates between **cozy isolation** — rain pattering on the windshield, wipers squeaking, radio playing indie folk — and **creeping dread**. Forest "groans and roars" ambientally; bushes rustle with invisible movement. Distant foghorn sounds carry through the fog.

Junction condition **"Eerie Darkness"** plunges everything into pitch black, transforming the experience into genuine survival horror. Combined with **"Something's Moving"** — where Tourists teleport more frequently and shadowy figures appear between trees — the Outer Zone becomes terrifying despite its surface beauty.

Emotional register: a place once ordinary, now haunted by something that doesn't follow the rules. Beauty — genuinely beautiful PNW forestscapes, golden light through rain, wildflowers growing through cracked asphalt — watched over by blank-faced mannequins and humming electrical ghosts.

## II. The Mid Zone

*Biomes: The Mires and The Scorch · Sectors C, D · Established 1961*

### Weather and atmospheric conditions

The Mid Zone is **darker, murkier, wetter** than the Outer Zone. In the Mires, thick mist and fog are near-constant, reducing visibility significantly and creating claustrophobic enclosure. **Corrosive Squalls** bring acid rain — visibly green-tinted precipitation that eats through metal. The rain itself becomes a weapon; droplets leave sizzling trails on the car's surface. The sky hangs lower, heavier, less differentiated — a uniform oppressive gray that rarely breaks.

In the Scorch, atmosphere shifts to a dry, heat-distorted quality — shimmer lines rise from charred ground, and the air carries a haze-like quality despite the lack of actual heat. Electromagnetic disturbances are more frequent and violent. The instability storm feels more urgent here — the yellow and red walls close faster, the environment resists your presence more actively.

### Terrain and landscape

**The Mires**: swampy waterlogged marshland — standing bodies of dark water, boggy ground that sucks at tires, partially submerged vegetation. The lush green of the Outer Zone has curdled into **sickly yellows and murky olive-greens**. Waterways thread between patches of solid ground. Roads are worse — more cracked, more submerged, more overgrown. Driving often means navigating between patches of solid terrain surrounded by black water.

- **Swamp Coral** — bioluminescent organic growths — punctuate the murk with eerie glows
- **Marsh Eggs** — alien organic material — cluster in waterlogged areas

**The Scorch**: burnt wasteland — dead trees stand as blackened skeletons against the sky, branches bare and charred. Ground is **charcoal-black**, cracked and ashen. Sparse surviving plant matter **glows like embers** — orange-red points of light scattered across a dark canvas. **Thermosap Crystals** — large glowing amber-red crystalline formations — protrude from the scorched earth, casting warm light across the blackened landscape. Terrain feels post-apocalyptic — as if flash-burned and never recovered. Groves of skeletal trees create stark silhouettes against the overcast sky.

Contrast between the two Mid Zone biomes is dramatic: one drowning, one burned, both hostile.

### Structures and architecture

Infrastructure in the Mid Zone is more **decayed and partially consumed** by the environment. Buildings in the Mires are half-submerged, lower stories underwater, rooftops barely visible above the marsh. Research facilities show more advanced degradation — walls crumbling, roofs collapsed, equipment corroded by decades of acid exposure.

The Scorch features more **industrial/research remnants** — larger facilities, heavier equipment, installations suggesting serious government investment before abandonment. Cargo containers along routes, some stacked haphazardly, others half-buried. Infrastructure reads as older than the Outer Zone — because it is. Sealed six years earlier (1961 vs 1967), the additional time shows in deeper corrosion, more structural collapse, more complete reclamation by the corrupted environment.

### Lighting and color palette

Mid Zone palette fractures into two distinct registers.

**The Mires**: dominated by **dark greens, murky browns, sickly yellows** — the color of stagnant water, rotting vegetation, corrosive chemicals. Bioluminescent Swamp Coral provides **acid-green glows** reflecting off standing water. Oppressive lighting with minimal contrast — everything uniformly dark and murky.

**The Scorch**: inverts to **blacks, deep charcoal grays, ember-orange accents** — palette of a dying fire. Thermosap Crystals cast **warm amber-red light** across blackened surfaces, creating dramatic contrast between glowing formations and surrounding darkness.

Energy signature of Mid Zone is **Unstable**, coded in **red** — significant visual escalation from Outer Zone's yellow. Overall lighting feels hostile — the beautiful diffused PNW light replaced by suffocating murk or harsh ember-glow. Shadows deeper, contrasts more extreme, comforting green of the forest gone.

### Anomalies and surreal elements

Mid Zone introduces **corrosive/organic anomalies** that feel more biologically disturbing.

- **Pickpockets** — pink translucent jellyfish-like floating creatures with bright green eyes and hanging tentacles — bioluminescent and alien, grab items from your hands or drag you through the air
- **Rotten Eggs** — yellowish acid-goop formations in various sizes — organic pustule-like blobs exploding on surfaces and sticking to the car
- **Moldy Balloons** — fly through the air spraying corrosive trails
- **Cough Boxes** — produce clouds of sickly green corrosive gas enveloping areas
- **Shaggy Scramblers** — mobile explosive threats moving toward the player
- **Belching Barnacles** — extend underground with surface protrusions
- **Spike Puddles** — dark black gooey substances on roads with shimmering purple spikes protruding upward
- **Acid-spitting nests** — launch corrosive projectiles at the car
- **Goo volcano** — shoots animal-shaped goo traps

Anomalies feel less mechanical and more organic — as if zone's corruption has begun generating its own biology. Visual language shifts from Outer Zone's "broken machinery" to something more like "diseased ecosystem."

### Mood and atmosphere

Emotional register shifts from Outer Zone's eerie-but-recognizable to **genuinely hostile and alien**. In-game character Tobias warns things here have *"been marinating in radiation for so long"* that *"things don't work the same."* Mood is **oppressive, claustrophobic, poisonous** — environment itself feels actively malevolent rather than passively strange.

In the Mires, standing water and limited visibility create a feeling of being trapped in a slowly drowning landscape. In the Scorch, charred desolation evokes post-catastrophe despair — a place where something terrible happened and the land never healed. Beauty-in-decay of Outer Zone largely absent; replaced by **grim corroded hostility** making even resource-gathering feel like desperation.

Contrast with Outer Zone is the point — the game deliberately escalates visual and emotional stakes to make the player understand that going deeper means losing the last traces of the familiar.

## III. The Deep Zone

*Biomes: Smokestacks and Red Spires · Sectors A, B, R · Established 1955*

### Weather and atmospheric conditions

Sky in the Deep Zone is **always overcast** — permanent oppressive ceiling of dark cloud that never breaks. Atmosphere denser, heavier, darker than anywhere else in the exclusion zone. **Meteorite Squalls** are the signature weather event — massive objects fall from the sky. These are not small rocks: **rectangular slabs of stone resembling double-wide trailers or building facades**, irradiated and trailing radiation plumes as they slam into the ground. Create a "concrete city of death" as they pile up around the player's route.

The instability storm here is fastest and most aggressive — the red wall of churning light closes with lethal speed. Electromagnetic disturbances are constant background noise. Air itself feels charged and wrong — visibility limited not by fog but by sheer density of atmospheric corruption. Every weather event in the Deep Zone feels apocalyptic in scale.

### Terrain and landscape

**Smokestacks biome**: industrial wasteland — prominent smokestack structures rise from ruins, heavy infrastructure dominates the landscape, terrain reshaped by decades of anomalous activity. Ground scarred, uneven, hostile. Roads barely recognizable beneath debris and geological upheaval.

**Red Spires biome**: game's most visually alien environment. **Towering red-glowing rock formations** — the eponymous spires — rise from the landscape like geological cathedrals, surfaces emanating deep crimson light. **Olympium deposits** — large gray pillars with glowing purple/magenta stones attached — protrude from the earth, creating an alien mineral garden. Ground scored with glowing cracks and anomalous scarring. Landscape departed entirely from anything recognizable as Pacific Northwest terrain — an **otherworldly hellscape** reading as alien geology transplanted onto Earth.

Biome name "Red Spires" doesn't adequately convey the visual impact: imagine Bryce Canyon formations made of glowing red stone, surrounded by purple crystal veins, under a permanently dark sky. The Deep Zone terrain represents the original 1955 test site — the place where everything went wrong first, and has had the longest to become something entirely other.

### Structures and architecture

Deep Zone contains the oldest infrastructure — **1950s-era research facilities** in advanced states of decay and anomalous corruption. Smokestacks biome features heavy industrial architecture — factories, processing plants, research installations with prominent chimney structures. **Red Meadow Research Facility** and similar ARDA installations are present but barely recognizable beneath decades of damage and anomalous transformation. Equipment and structures are partially fused with geological formations. The boundary between built environment and corrupted landscape blurs — walls merge into rock faces, machinery sprouts crystal growths, corridors collapse into geological features.

**The Well**, the final location in the deepest reaches, abandons all pretense of normal architecture — described as **"a bizarre maze of television screens"** where fragments of past conversations play, existing as pure anomalous abstraction rather than any recognizable built environment.

### Lighting and color palette

Deep Zone palette undergoes the game's most dramatic shift. Dominant color is **red** — deep crimson, rust, blood. The entire atmosphere takes on a reddish cast, so pervasive it tints the player's garage lighting when they return. Community members report *"the ambient lighting and sky of my garage has turned to red spires red."* Corrupt energy of this zone glows **magenta/purple** — sickly otherworldly color signaling maximum danger.

In Smokestacks: palette combines **industrial grays with the red shift** — rusted, irradiated industrial aesthetic. In Red Spires: red dominates everything — glowing rock formations, crimson-tinted sky, purple crystal accents. Lighting is **harsh and directional** from anomalous sources rather than natural — no sunlight penetrates the permanent overcast, so illumination comes from glowing spires, purple crystal deposits, and the player's headlights.

Color progression across the entire game — **blue-green (Outer) → murky green/ember orange (Mid) → crimson/magenta (Deep)** — is one of the most effective visual storytelling devices, making the player feel the escalation in their bones.

### Anomalies and surreal elements

Deep Zone anomalies are the most spectacular and dangerous.

- **The Blacksmith** — massive heap of glowing blocks and smooth rocks that emits a **pink/light purple glow** when activated — shakes the ground violently and sends rows of jagged stone protrusions (**Titan's Bones**) radiating outward from its position, spikes erupting from the earth in spreading lines. Glowing marks appear on the ground before the spikes emerge.
- **Can Openers** — giant saw blades that travel back and forth along **glowing red track-lines**, literally **cutting through the earth itself** — the visual of the ground being sliced open is deeply unsettling.
- **Fallen Firmament** — drops massive flat irradiated monoliths from the sky during Meteorite Squalls — alien debris crashing through whatever remains of the canopy.
- **Tour Buses** — grotesque amalgamations of fused Tourists — legs melted into a shared base, bodies arranged in the **shape of two outstretched hands** holding a larger dummy in the center, arms reaching upward, emitting a **bright red glow** and ominous low hum.
- **Boom Bunnies** — angry red-lit explosive scrap creatures bouncing wildly and detonating on contact.
- **Devil Grinders** — destroy tires.
- **Radiant Dredges** — cause punctures, indicated by **purple glow on concrete pillars**.
- **Wriggling Wrecks** — animate car husks moving with disturbing intent.

Anomalies no longer feel like glitches in reality; reality has been completely overwritten.

### Mood and atmosphere

Deep Zone achieves what community members describe as **genuine Lovecraftian wrongness** — a sense that the environment is not merely damaged or corrupted but has become something fundamentally alien that was never meant to be experienced by human consciousness. The red-dominated palette, permanent dark sky, alien geological formations, and sheer density of hostile anomalies create an atmosphere of **maximum hostility and alienation**. Players report increased heart rates and genuine fear.

Mood is not the creeping dread of the Outer Zone or the oppressive hostility of the Mid Zone — it is **existential confrontation with the incomprehensible**. Yet there remains a terrible beauty: the glowing red spires against the dark sky, the purple crystal formations catching light, the spectacular violence of Meteorite Squalls raining alien monoliths. The Deep Zone embodies the game's aesthetic thesis at its most extreme — the Pacific Northwest transformed beyond recognition into something simultaneously terrifying and awe-inspiring.

## IV. The Expansion Walls and Barriers

### The physical wall between zones

Expansion Walls are **massive concrete/industrial barriers** of enormous scale — think Hoover Dam-sized construction spanning the landscape from horizon to horizon. **Construction cranes** remain affixed to the wall's upper edges, frozen mid-build — evidence the barrier was erected hastily and never properly completed. Walls are weathered, stained with decades of rain and radiation exposure, concrete surfaces cracked and discolored.

From a distance, they read as titanic gray-brown slabs cutting across the landscape, disrupting natural topography with brutal industrial geometry. They represent the most visible evidence of government intervention — monuments to containment built by an organization (ARDA) that ultimately failed.

### The storm drain crossing

Passage between the Outer and Mid Zones is through a **storm drain opening** — a large pipe entrance lit by a **single red light**. Interior is **extremely dark** — headlights essential and barely adequate. Inside, architecture shifts to what resembles a **railway station or dockyard**: cargo containers stacked in corridors, industrial pipes carrying coolant along walls, metal catwalks and grating. Pockets of radiation glow in corners. Path winds through darkened tunnels with minimal lighting — bare industrial bulbs, emergency strips. Atmosphere claustrophobic, industrial, threatening. Emerging on the Mid Zone side feels like crossing a threshold from one reality into a worse one.

### The Old Wall crossing into the Deep Zone

Deep Zone crossing involves a **circuit-connection mechanic** — player drives between checkpoint gates that glow **green** when active and shift to **orange** as they expire. **LIM Shield** creates a visible energy barrier around the car during this sequence — shimmering translucent forcefield enveloping the vehicle. Visual effect combines industrial decay (oldest barrier, most corroded) with active energy effects (shield, checkpoint gates, electromagnetic interference). Passage feels more desperate and unstable than the Outer-to-Mid crossing — infrastructure older, more damaged, anomalous activity palpable even in the transitional space.

## V. The World Outside the Exclusion Zone

### The broader Pacific Northwest context

Game set in **1998** — world outside the OEZ is late-'90s Pacific Northwest America. OEZ established in **1955** after catastrophic government experiment; outside world has lived alongside this sealed-off anomalous territory for over four decades. Setting implies **small Pacific Northwest towns** — logging communities, coastal villages, the kind of places where rumors about what's inside the zone circulate as local folklore.

1998 temporal setting allows a specific technological aesthetic: no smartphones, CRT monitors, analog equipment, cassette tapes and early CD players, chunky plastic electronics. World outside presumably features normal beauty of the Olympic Peninsula — **temperate rainforests, rugged Pacific coastline, snow-capped Olympic Mountains, small towns with main streets and lumber mills** — all existing in unsettling proximity to a 3,600-square-mile exclusion zone full of impossible phenomena.

### The player's garage and base camp

Home base is an **abandoned auto shop** — last safe haven inside the zone. Features a **large garage bay** with the station wagon parked inside, surrounded by crafting stations, workbenches covered in tools and salvaged components. A **jukebox** provides music. A **route-planning map** dominates one wall.

Aesthetic: *"mechanic's workshop meets survivalist bunker"* — organized chaos, with shelves of parts, cables hanging from the ceiling, improvised lighting, warm glow of work lamps. Garage exists in a pocket of stability within the zone — outside its walls, the anomalous environment presses in. Contrast between the garage's domestic, warm-lit, workshop comfort and the hostile world outside its doors is a critical emotional anchor.

Garage's ambient lighting and sky actually shift to match the deepest zone the player has accessed — turning red when the Deep Zone's Red Spires have been visited, visually contaminating even this safe space.

### The retro-tech UI aesthetic

Interface design reinforces 1998 temporal setting. Player's **AR headset** is designed as a hacked-together PDA — UI inspired by **Windows 3.1** aesthetics with pixelated fonts, thick angular lines, and "home office" OS-like icons. Menus resemble **tarnished scraps of paper** — physical documents from a time before the exclusion zone. Icon system uses "chunky and readable" designs based on household objects — rubber ducks, plastic cups — rather than abstract sci-fi symbols.

Retro-tech aesthetic extends to in-game equipment: analog gauges, physical switches, cassette-based recording devices. Grounds the experience in a specific nostalgic technological moment contrasting sharply with the impossible phenomena the technology is being used to navigate.

## VI. The Station Wagon — A Visual Character Study

### Base aesthetic and design lineage

Car is a **fictional 1983 "Radius Motors" station wagon** — a composite of American "woodie" wagons from the late '70s and '80s. Primary design inspirations: **1989 Buick LeSabre Estate**, **Volvo 700-series wagon**, **1979 Ford Country Squire**, **Mercury Colony Park**. Interior resembles a **1980s Mercury Zephyr**. License plate reads **"MELIC HT"** (an anagram of "LIM TECH") on a Washington 1947 plate.

Starting appearance: **beat-up woodie wagon** — faux wood-grain paneling along the sides, chrome bumpers with surface pitting, rectangular headlights behind cracked lenses, boxy silhouette that screams family road trips and suburban driveways. Car begins as a mishmash of Crude and Steel parts — improvised, barely functional, held together by determination and anomalous energy.

### The car as living anomaly

Wagon is a **"Remnant"** — living anomaly bonded to the driver. **Strange pulsing veins** run across certain components, most visible under the hood when the engine is removed and in the **"trunk gunk"** — organic vein-like substance holding the small gas can in place. Veins mirror organic structures found on anomalous formations throughout the zone.

Car develops **"quirks"** — emergent if-then behaviors from prolonged zone exposure: headlights dimming when the steering wheel turns, doors flying open during acceleration, the hood popping up to block the driver's view, the horn blaring when the trunk closes. Quirks make the car feel alive — an unpredictable partner rather than a mere vehicle.

### Visual progression and damage aesthetics

Car's visual journey follows what developers called the **"Doc Brown in a rush"** principle — the aesthetic of improvised science, of equipment that is functional but hastily assembled. Progression moves from **Crude** (rough, scavenged, barely held together) through **Steel** (industrial gray, functional) to specialized variants:
- **Insulated** (electrical resistance — rubberized, insulated surfaces)
- **Lead-Plated** (radiation resistance — heavy, dull-gray plating)
- **Anticorrosive** (acid resistance)
- **Armored** (impact protection — visible reinforcement)
- **Carbon Fiber** (lightweight, modern)
- **Olympium** (endgame material — **purple-tinged**, incorporating the zone's rarest mineral)

Damage is individual and granular — panels dent and crack independently, tires pop and deflate (forcing driving on bare rims), windshield spiders with cracks, doors can be torn away entirely, and the hood can be destroyed. Damage state of the car at any moment tells its own visual story of what the player has survived.

### Dashboard and interior atmosphere

Interior experienced exclusively in **first person** — player sees the world through windshield and side windows, creating intimate claustrophobic immersion. Dashboard features **functional analog gauges** glowing amber — fuel, battery, health, shield condition. Car requires physical interaction: **turning the ignition key, shifting gears, cranking windows, toggling headlights and wipers**.

Cosmetic customization allows fuzzy dice hanging from the rearview, dashboard bobbleheads, 8-ball shifter knobs, pirate ship hood ornaments, antenna toppers (including a buttered toast slice). Radio cycles through stations playing rock, country, indie folk, synthwave, ethereal vocals — soundtrack providing emotional counterpoint to visual chaos outside.

Overall interior aesthetic: **"beloved beater car"** — personal, lived-in, comforting in its familiarity, even as the windshield frames impossible horrors.

### The car in its environment

Central visual tension of Pacific Drive: **contrast between the battered domestic station wagon and the hostile alien landscape it traverses**. A wood-paneled family wagon bristling with improvised sci-fi equipment — lightning rods, LIM pulse emitters, ion shields, radar antennae, side racks — careening through acid-scarred marshlands and crimson alien spire formations.

Headlights cut weak yellow cones through dense fog, catching rain droplets and illuminating Tourists standing motionless on the road ahead. Narrow first-person view amplifies disorientation during extreme moments: being launched airborne by Airstrip anomalies, being dragged sideways by Abductors, careening through acid rain with wipers going haywire, racing toward a **massive pillar of orange-white exit light** erupting from the ground while the red storm wall churns behind.

Available paint jobs reinforce the aesthetic range: **Aurora Frost** (shimmering green-blue Northern Lights gradient), **Rust & Gold** (weathered metallic), **Neon Dreams** (black base with glowing neon stripes that pulse in dim light). The car is simultaneously the most mundane and most extraordinary object in the game world.

## VII. Prompt-Ready Visual Descriptions

### Image generation reference passages

**The cozy-dread drive**: A battered 1980s wood-paneled station wagon with mismatched steel body panels and improvised sci-fi equipment on its roof rack — lightning rods, antenna arrays, glowing sensors — drives down a rain-slicked two-lane forest highway at dusk. Headlights cut weak amber cones through dense Pacific Northwest fog. Towering Douglas firs line both sides, their trunks disappearing into mist. Rain spatters the cracked windshield. Wipers mid-sweep. The road ahead is empty but feels watched. Muted teal-green color grading. Stylized realism. Pastel atmospheric tones. The mood of Twin Peaks meets Stalker.

**Tourist encounter**: A beaten station wagon's headlights illuminate a semicircle of featureless beige crash-test-dummy humanoid figures standing motionless on an overgrown forest road. Their smooth faces are blank. Ball joints visible at elbows and knees. A dim pulsing red glow emanates from each chest. Their feet sink into tar-like black substance fusing them to the asphalt. Dense fog behind them. Dead trees. One figure's arm is raised in a frozen salute. The car's paint is scratched and dented. Atmosphere of dread and wrongness. Night scene, headlight illumination only.

**The storm escape**: A station wagon races at maximum speed down a crumbling forest road toward a massive pillar of blinding orange-white light erupting from the ground into the sky. Behind the car, a wall of churning red swirling energy consumes the landscape — trees being torn apart, debris spinning in the air, the ground glowing red. Yellow-tinted radiation zone transitions to hellish crimson. Blue and orange lightning flashes within the red wall. Air-raid siren mood. Desperate velocity. The car's rear bumper sparks against road debris.

**The Mires**: A station wagon navigates a waterlogged marshland road — standing bodies of dark water on both sides, partially submerged dead trees, bioluminescent swamp coral glowing acid-green beneath the surface. Thick fog limits visibility. The sky is a uniform oppressive gray. Sickly yellow-green color palette. Jellyfish-like pink anomalies float in the middle distance, translucent and bioluminescent. The car's paint is corroded, panels pocked with acid damage. The mood is suffocating, poisonous, claustrophobic.

**The Scorch**: A lone station wagon on a road through a charred wasteland — blackened skeletal trees against a dark overcast sky, ground cracked and ashen. Scattered among the darkness, surviving plant matter glows like dying embers — orange-red points of light. Large amber-red crystalline formations (Thermosap Crystals) protrude from the scorched earth, casting warm light across the blackened landscape. The palette is charcoal, black, and ember-orange. Post-apocalyptic beauty. The car's headlights reveal the devastation ahead.

**Red Spires vista**: A battered station wagon on a winding road through an alien landscape — towering red-glowing rock spire formations rise against a permanently dark overcast sky. Purple-magenta crystal veins grow from gray stone pillars. The ground is scored with glowing pink cracks radiating outward from a distant anomalous formation. The entire atmosphere is tinted crimson. The car, with its wood paneling and improvised equipment, looks impossibly mundane against this otherworldly hellscape. The mood is awe and existential dread in equal measure.

**The Expansion Wall**: A massive concrete barrier stretches from horizon to horizon across a forested Pacific Northwest landscape. Construction cranes remain affixed to its upper edge, frozen mid-build. The concrete is weathered, stained with decades of rain and anomalous exposure. At its base, a storm drain opening glows with a single red light. A station wagon approaches, headlights on, dwarfed by the wall's scale. Dense forest presses against both sides of the barrier. The mood is governmental brutalism versus wild nature — containment architecture on a titanic scale.

**The Blacksmith eruption**: A massive formation of glowing blocks and smooth rocks emits intense pink-purple light from its core. Jagged stone spikes erupt from the earth in radiating lines, spreading outward in all directions. Glowing marks on the ground precede each spike's emergence. A station wagon swerves desperately between the emerging formations. Debris and dust fill the air. The Red Spires biome's crimson rock formations loom in the background. The entire scene is chaos rendered in reds, pinks, purples, and stone grays. Deep Zone atmosphere — alien, hostile, spectacular.

**Dashboard intimacy in rain**: First-person interior view of a 1980s station wagon dashboard — amber-glowing analog gauges, a fuzzy dice hanging from the rearview mirror, a small bobblehead on the dash. Rain hammers the cracked windshield — wipers smear water across fracture lines. Through the glass: a pair of floating Abductor drones with green scanning light-cones hover above a dark forest road. The radio dial glows softly. The car's battery gauge is low. Outside, the world is hostile and alien. Inside, it's almost cozy. The contrast is everything.

### Video generation reference passages

**Slow approach through fog (5–10 seconds)**: Camera follows behind a station wagon driving slowly through dense Pacific Northwest fog on a narrow forest road. Headlights create amber cones in the mist. Trees materialize and pass. Rain beads on surfaces. The car's brake lights glow red as it slows. Something stands in the road ahead — a humanoid silhouette, motionless. The car stops. The figure doesn't move. Atmospheric sound: rain, engine idle, distant foghorn.

**Storm chase sequence (10–15 seconds)**: Camera mounted on car's rear, facing backward. A churning wall of red energy advances, consuming forest behind the car. Blue-orange lightning flashes within the wall. Trees are torn apart at the boundary. The car accelerates — road debris scatters, tires squeal on wet asphalt. Camera pans forward to reveal a pillar of white-orange light ahead — the exit gateway. The car races toward it. The red wall gains. Impact with the light column — white flash to black.

**Zone transition montage (15–20 seconds)**: Series of dissolving landscape shots showing the progression: lush green PNW forest with rain → murky swamp with bioluminescent green glows → charred blackened landscape with ember-orange points → towering red rock spires under dark sky with purple crystal formations. Each transition increases the color saturation toward red. A station wagon appears in each shot, growing progressively more battered and modified. The music shifts from ambient folk to dissonant electronic drone.

## The Thread Connecting Beauty to Horror

Pacific Drive's visual achievement is not any single element but the **gradient between them**. The game begins in a place you might actually visit — the rainy forests of Washington's Olympic Peninsula, where moss covers everything and the trees are older than nations. It ends in a place that could not exist — crimson spire formations under a permanently dark sky, where the ground erupts in radiating lines of stone and rectangular slabs of rock fall from the clouds like alien rain.

Between those extremes, the game maps a continuous visual degradation: green becomes sickly yellow, rain becomes acid, forests become marshes become charred wastelands become alien geology. And through all of it drives a wood-paneled 1980s station wagon held together with tape, improvised science, and what might be the car's own living will to survive.

Developers drew from Stålenhag's kitchen-sink sci-fi, VanderMeer's New Weird, and the genuine atmosphere of the Pacific Northwest's "very, very old parts of nature" to create a visual vocabulary that is unique in games — familiar enough to feel like a place you've been, strange enough to feel like a place that shouldn't be. That tension between recognition and alienation, between the cozy glow of a car radio and the blank stare of a crash-test mannequin in your headlights, is what makes Pacific Drive's visual identity so potent for creative reference.

## Why This Matters for Old Growth

- **Shared setting register**: Old Growth's PNW (a fictional ancient forest) and Pacific Drive's OEZ (a fictional Olympic Peninsula) share moss, rain, dense conifers, and the weight of a place older than human presence.
- **Shared premise**: a single vehicle, a hostile world, and an inability to defeat the environment through escalating capability.
- **Shared gradient**: Pacific Drive escalates from rainy forest → murky marsh → alien spires; Old Growth escalates from [[Biomes|Verdant → Fungal Depths → Petrifold]]. Both use the gradient itself as the horror.
- **Shared domestic anchor**: both have a garage — Pacific Drive's "abandoned auto shop" as safe haven vs. Old Growth's garage (see [[Narrative Direction]]).
- **Key difference**: Pacific Drive's anomalies are **mechanical/biomechanical** (UFOs, crash-test-dummies, saw blades). Old Growth's anomalies are **biological/geological** (mycelium, spores, petrification).
- **Key difference**: Pacific Drive's OEZ has a **government narrative** (ARDA, test site, history). Old Growth's forest has **no history** — no author, no cause, no intent. The forest is older than narrative.
- **Key difference**: Pacific Drive's car develops **upgrades and quirks** (progression with personality). Old Growth's Samba carries **commitments as tradeoffs** (Article IV Rule 8) — different configuration, not greater capability.

## Related

- [[Visual Direction]] — Old Growth's canonical art direction
- [[Palettes]] — per-biome palette decisions
- [[Biomes]] — the gradient that drives palette shifts
- [[Narrative Direction]] — the garage/vehicle/world triangle
- [[Tone and Horror]] — the tonal discipline
- [[Rock Scatter Design]] — concrete art decision for biome identity
- [[Art]] — folder MOC
ember's answer for note_placement-art-pacific-drive-reference, placementProbability ember gave each option.Art0.05 goldCode0.01Decisions0.01Design0.03Discoveries0.01Governance0.01Narrative0.01Reference0.86 ember's pickSessions0.01Systems0.01

note_placement-reference-code-todos: placement

Note placement · dev
Gold
Place the note under Reference/ (Reference)
ember
Place the note under Code/ (Code)
Signal
confidence 0.88 (trust band)
Kit outcome
acted on a wrong answer
Why this gold label
Mined from Reference/Code TODOs.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Code TODOs

A consolidated index of `TODO`, `FIXME`, `HACK`, and `NOTE` comments harvested from the Old Growth codebase during Phase 4 (codebase archaeology). Each entry includes the source file, approximate line, and category so they can be triaged into dedicated fix PRs.

<!-- Initial entries added from Phase 4 archaeology. A systematic TODO/FIXME/HACK/NOTE sweep of src/scripts/ and rust/ is a separate, non-blocking documentation effort. -->

## Entries

### Documentation gaps flagged during Phase 3-4 authoring

These are places where the source documentation or code comments did not fully specify a behavior, and the vault note had to be authored from observed code behavior or design inference. See individual note `UNDOCUMENTED` callouts.

- **[[Audio — Forest Transmissions]]** — generation approach (composed / procedural / hybrid) not established in source spec
- **[[Audio — Pipeline]]** — boundary between spoken pipeline (`tools/tts/`) and music pipeline is not explicitly drawn in source docs
- **[[Tools — Observer]]** — full source walk deferred; note is a design-level stub with detail to be filled later
- **[[Rock Scatter Design]]** — source doc's open questions (LOD, shadow culling, tree interaction, Petrifold unique coloring) preserved in the vault note

### TODO / FIXME / HACK / NOTE sweep

A systematic sweep of TODO/FIXME/HACK/NOTE comments across `src/scripts/` and `rust/` is separate, non-blocking, and can be done independently of the vault scaffolding work. This note will be expanded when that sweep is undertaken.

## Related

- [[Reference]] — folder MOC
- [[Code]] — code architecture notes that flag these TODOs
ember's answer for note_placement-reference-code-todos, placementProbability ember gave each option.Art0.01Code0.88 ember's pickDecisions0.02Design0.02Discoveries0.02Governance0.01Narrative0.01Reference0.02 goldSessions0.01Systems0.01

note_placement-systems-audio-radio-stations: placement

Note placement · dev
Gold
Place the note under Systems/ (Systems)
ember
Place the note under Design/ (Design)
Signal
confidence 0.63 (verify band)
Kit outcome
acted, flagged for verification
Why this gold label
Mined from Systems/Audio — Radio Stations.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Audio — Radio Stations

The complete genre case study, per-genre sonic specification, reference artists, Suno prompt keyword libraries, production characteristics, mood/emotional vocabulary, and target genre distribution for Old Growth's radio content. Derived from analysis of the Pacific Drive soundtrack (functionally adjacent) with adaptations for Old Growth's PNW ancient forest setting.

## Overview

Old Growth's radio spans 8 genre lanes across ~13 stations. Each lane has a distinct sonic identity, reference artists, recommended instrumentation, production character, and emotional register. The lanes are not interchangeable — each occupies a specific slot in the player's emotional journey through the forest. This is an authored creative direction document, primarily used as Suno prompt guidance when generating music content.

See [[Audio — Car Radio]] for the station roster and frequency assignments; see [[Audio — Pipeline]] for the TTS + post-processing pipeline that produces spoken content for these stations.

## Design Intent

The radio is the player's only human voice in the world. Station variety is not about providing variety as an abstract good — it is about giving the player's emotional state a dial. Dark folk is the dread lane. Haunted Americana is the lonely-driving lane. Indie rock is the energy/escape lane. Dream pop is the beauty lane. Dark gospel is the devastation lane. When a player chooses a station, they are choosing the mood they want to hear themselves inside of.

All genre descriptions are Suno prompt guidance — descriptive, specific, focused on sonic characteristics, instrumentation, production style, and emotional vocabulary.

## Genre 1 — Dark Folk / Neofolk

**Pacific Drive examples**: King Dude ("Forty Fives Say Six Six Six", "The Castle"); Of The Wand And The Moon ("Whispers of the Past").

### Sonic description

The most unsettling genre on the soundtrack. Stripped to almost nothing — single acoustic guitar, sometimes barely strummed, sometimes fingerpicked in slow deliberate patterns. Vocals sit low: deep, gravelly, half-whispered baritone that sounds like someone talking to themselves in a dark room. Heavy reverb gives a cavernous, disembodied quality, as if echoing off stone walls or the inside of a hollow tree.

Tempo is glacial. Beats are sparse and martial when present — a single kick or hand drum hitting the downbeat with long silences between. Sometimes no percussion at all, just the rhythm of the guitar strum and the natural cadence of the vocal phrasing.

### Instrumentation

- **Acoustic guitar**: nylon or steel-string, played simply. Sometimes a single repeated chord. Sometimes a minimal two-chord progression. Old, woody, slightly dead tone — not bright or ringy. Think of a guitar with old strings in a cold room.
- **Vocals**: deep male baritone or low tenor. Whispered, spoken, or half-sung. Drenched in reverb. Delivery incantatory — repetitive phrases, like a prayer or spell. Think Leonard Cohen at his most skeletal, or Johnny Cash filtered through European post-industrial.
- **Occasional viola or cello drone** beneath the guitar — a single sustained low note that adds weight and darkness without melodic movement.
- **Sparse synthesizer pads**: dark, analog-sounding, sitting far back in the mix. Not melodic — just texture. A low hum or slow pulse.

### Production character

Lo-fi and intentionally primitive. The recording should sound like it was captured in a single room with minimal equipment. Tape hiss or analog warmth welcome. No polish, no compression, no modern sheen. The reverb is the only prominent effect — cavernous, like a church or a cave.

### Mood / emotional vocabulary

Occult, sinister, ritualistic, desolate, nocturnal, blasphemous, resigned, hypnotic, sacred-profane. Walking alone on a logging road at 2 AM. Something watching you from the treeline. An old campfire story told by someone who believes it. Southern Gothic transported to the PNW — swap the swamp for old-growth forest, the bayou for a fog bank.

### Reference artists (beyond Pacific Drive)

Death in June, Current 93, Nick Cave (murder ballads era), 16 Horsepower, Bonnie Prince Billy, Wovenhand, Chelsea Wolfe (acoustic material), Songs: Ohia.

### Suno prompt keywords

`dark folk, neofolk, acoustic, reverb-drenched vocals, baritone whisper, minimal, slow tempo, gothic americana, lo-fi, sparse percussion, occult folk, atmospheric, haunting, single acoustic guitar, droning viola`

## Genre 2 — Haunted Americana / Gothic Singer-Songwriter

**Pacific Drive examples**: Claire Cronin ("Bloodless"); A Shell In The Pit ("Ghost on the Road"); I Will Keep Your Ghost ("1964"); Old Man Luedecke ("She Told Me Where To Go"); Ilene Dracott ("Train Song").

### Sonic description

The emotional core of the soundtrack — the "lonely highway" sound. Where dark folk is ritualistic and sinister, haunted Americana is personal and sorrowful. Arrangements spare but slightly more developed than neofolk: electric guitar with reverb and subtle distortion (not fuzzy — natural breakup of a tube amp turned up just past clean), sometimes fingerpicked acoustic, and a spectral vocal that sits in the mid-range.

Vocals are the focal point. Exposed and vulnerable — sometimes a woman's voice about to break, sometimes a man's voice with a weary, road-worn quality. Singing is conversational, not performative. Words delivered plainly, as if confessing to a stranger. Harmonies, when they appear, are ghostly — a second voice just barely audible, doubling at the octave or third.

Tempo: slow to mid-tempo. Often a steady hypnotic pulse — walking-pace rhythm that gives the feeling of forward motion through a dark landscape.

### Instrumentation

- **Electric guitar**: clean or lightly overdriven, heavy on spring reverb and sometimes tremolo (amp effect). Fender amp just on edge of breakup. Tone is glassy, watery, sustain bleeding into silence between notes.
- **Acoustic guitar**: fingerpicked arpeggios or simple strumming. Warm, not bright.
- **Vocals**: the centerpiece. Vulnerable, breathy, slightly nasal or reedy. Female voices in this lane tend toward Cat Power / Mazzy Star register — low, smoky, unhurried. Male voices channel Jason Molina or early Bon Iver — cracked, intimate, with distance.
- **Viola or violin**: not fiddling — long sustained bowing. Mournful legato weaving around the vocal. Key textural differentiator from pure folk. Classical, almost funereal gravitas.
- **Sparse synth pads**: analog warmth, slow attack. Bed texture, not melodic.
- **Minimal drums** when present: brushes on snare, soft kick, or just a shaker. Nothing assertive.

### Production character

More produced than dark folk but still intimate. Feels like a bedroom or small studio — close-miked vocals with audible breath, room tone present but not dominant. Medium-length reverb gives space without becoming cavernous. Warmth in the low end from tape or tube saturation. Narrow dynamic range — everything at a similar hushed volume, like a conversation in whispers.

### Mood / emotional vocabulary

Melancholic, wistful, spectral, confessional, lonely, aching, twilight, autumnal, resigned but not hopeless. Driving alone at dusk on a two-lane highway with headlights just coming on. Missing someone who might be dead. Beauty in decay — collapsing barn, rusted car in a field, lichen on a gravestone. Someone who has seen something they can't explain trying to process it through song.

### Reference artists

Jason Molina / Songs: Ohia, Cat Power, Mazzy Star, Mark Lanegan, Elliott Smith, Iron & Wine (early), Grouper, Emma Ruth Rundle, Marissa Nadler, Angel Olsen (early acoustic), Nick Drake.

### Suno prompt keywords

`haunted americana, gothic folk, spectral vocals, reverb electric guitar, fingerpicked acoustic, viola, mournful, slow tempo, singer-songwriter, intimate recording, lo-fi warmth, melancholic, twilight mood, sparse arrangement, confessional, Pacific Northwest`

## Genre 3 — Indie Rock / PNW Garage-Psych

**Pacific Drive examples**: Smokey Brights ("Conjuring Hell", "Save Us Sarah", "Fancy Ketchup", "Sad Boy Song #39"); Worse in Person ("The Freeze"); Crooked Mouth ("Decay", "Knife"); Michael Ray and the Plastic Sheets ("Seven Trumpets").

### Sonic description

Energy injection. Propulsive, loud, fuzzy. PNW rock in the lineage from the Sonics through Mudhoney through 2010s garage-psych revival. Guitars dominant — distorted but not metal-heavy. Cranked Fender or Vox amp with a fuzz pedal stepped on hard. Crunchy, warm, slightly unhinged.

Vocals shouted or belted — not screamed, but delivered with conviction and urgency. Male and female voices often trade off or harmonize in a raw, untrained way prioritizing feeling over technique. Hooks are big and undeniable — choruses that stick, even when lyrics are dark.

Tempos mid-to-fast. Drums loud in the mix, straight-ahead rock beats with fills more enthusiastic than technically complex. Bass melodic and prominent — countermelody to the guitar riff rather than just following root notes.

### Instrumentation

- **Electric guitars (multiple)**: heavy distortion, fuzz, or overdrive. Power chords, palm-muted chugging, big open chords on the chorus. Sometimes cleaner guitar providing jangly arpeggios under the fuzz. Wah pedal and phaser for psychedelic color.
- **Bass guitar**: round, growly, picked for attack. Melodic lines that move independently — Pixies or early Modest Mouse.
- **Drums**: snare-heavy, played hard. Crash cymbals on chorus downbeats. Midrange-punch kick. Tom fills loose and slightly behind the beat for swinging human feel.
- **Keyboards/organ**: Farfisa or Vox Continental-style organ — sustained chords or swirling psychedelic texture. Not piano. The "60s spy-movie" element in Smokey Brights' descriptions.
- **Vocals**: belted, harmonized, three or four voices stacking. Choir-like quality when all members sing together — not polished, but powerful.

### Production character

Loud, compressed, levels pushed into the red. Everything competes for space — guitars hard-panned L/R, vocals slightly overdriven through the preamp, drums close-miked with room mics for live feel. Bleed between instruments. Should sound like a band in a room playing together, captured with good mics but minimal post. Early Sub Pop "recorded in a basement" aesthetic, but with better fidelity.

### Mood / emotional vocabulary

Defiant, ecstatic, reckless, cathartic, anthemic, sweaty, combustible, celebratory-in-the-face-of-doom. Driving too fast on a dirt road at night because something is chasing you and you've decided you're not going to die tonight. The release valve. The moment in a horror movie where the protagonist stops running and turns to fight. Joyful in a desperate, this-might-be-the-last-song-I-ever-hear way.

### Reference artists

Mudhoney, The Sonics, Ty Segall, Thee Oh Sees (Osees), Black Lips, Shannon and the Clams, La Luz, Acid Tongue, Dead Moon, Wipers, The Melvins (lighter), Tame Impala (early/rawer), King Gizzard (garage material).

### Suno prompt keywords

`garage rock, psych rock, PNW indie, fuzz guitar, driving drums, shouted vocals, organ, lo-fi, punk energy, fast tempo, distorted, anthemic chorus, multiple vocal harmonies, bass-heavy, retro, 90s alternative, raw recording`

## Genre 4 — Dream Pop / Ethereal

**Pacific Drive examples**: Lemolo ("Swansea"); LEMON BOY ("Puzzle Pieces"); EXES, Petey ("Bloodoath"); Gaia Consort ("Drawing Down the Moon"); Moon Cathedral ("Turn the Page").

### Sonic description

Most beautiful and least threatening lane on the soundtrack — but carries its own unease. Not cheerful or bright. Oceanic, enveloping, slightly melancholic. Standing on a rocky PNW beach in fog — can hear the waves but can't see more than fifty feet. Everything diffused.

Hallmark: layers of reverb and delay on every element, creating a wash where individual instruments blur. Guitars clean or lightly chorused, playing arpeggiated patterns that shimmer and sustain. Vocals float above — often a female voice, high and clear, drenched in enough reverb to make words indistinct. You feel the melody more than you parse the lyrics.

Tempo slow to mid, with drift rather than drive. Rhythm section soft — brushed or programmed drums that pulse rather than hit.

### Instrumentation

- **Electric guitar**: clean tone with heavy chorus, reverb, delay. Arpeggiated chords or single-note lines ringing and overlapping. Cocteau Twins guitar sound — notes become textures, not discrete events. Sometimes acoustic layered underneath for warmth.
- **Synthesizers**: lush pads, slow-attack strings, shimmering high-frequency textures. Analog or analog-modeled. The bed everything else floats on.
- **Vocals**: female, ethereal, reverb-heavy. Smooth legato — notes bleed into each other. Sometimes layered, panned across the stereo field, creating a choral effect from a single voice.
- **Vibraphone or glockenspiel**: occasional melodic accents sparkling through the wash.
- **Strings (violin, viola, cello)**: long sweeping lines. Not sawing or rhythmic — sustained, swelling. Classical weight and emotional depth. French horn or similar brass providing distant warm color.
- **Bass**: round and deep, often fingered for soft attack. Simple root-note patterns, providing foundation without attention.
- **Drums**: brushed snare, soft kick, tip-of-stick hi-hat. Or programmed beats with soft rounded transients. Rim clicks instead of full snare. Drums suggest rhythm more than assert it.

### Production character

Polished and layered but not clinical. Mix is wide — instruments spread across the stereo field with generous panning and spatial effects. Low end warm and full, high end sparkles without being harsh. Every element bathed in reverb, but controlled, musical reverb — not sloppy. Sounds emerging from and receding into a fog of harmonics. Most "produced" lane on the soundtrack. Compared to lo-fi recording philosophy of other genres, dream pop benefits from mix care.

### Mood / emotional vocabulary

Ethereal, oceanic, wistful, transcendent, suspended, dreamy, liminal, tidal, bittersweet, vast, longing, weightless. Walking through old-growth forest in heavy fog at dawn where the canopy is so dense it creates its own weather. Between waking and sleeping. Grief moved past sharp pain into diffuse ambient sadness. Water — always water. Tidal pools, rain on the windshield, rivers heard but not seen.

### Reference artists

Beach House, Cocteau Twins, Mazzy Star, Julee Cruise / Angelo Badalamenti (Twin Peaks), Warpaint, Slowdive, Grouper, Cat Power (You Are Free era), Alvvays, Japanese Breakfast (softer), Widowspeak.

### Suno prompt keywords

`dream pop, ethereal, reverb-heavy, female vocals, shimmering guitar, lush synth pads, slow tempo, chorus effect, arpeggiated, oceanic, layered production, strings, ambient, atmospheric, melancholic, beautiful, PNW, foggy, shoegaze-adjacent`

## Genre 5 — Vaporwave / Retro Synth Instrumental

**Pacific Drive examples**: Eyeliner ("Los Angeles"); Zachary Blystone ("Liminal Drifter", "Prologue"); LRITDL ("Night Shades").

### Sonic description

Most sonically distinct lane — no acoustic instruments, no live band feel. Pure synthesizer music in a nostalgic uncanny valley. Eyeliner's "Los Angeles" is the clearest representative: MIDI-composed instrumental sounding like reimagined incidental music from a late-80s detective show or a point-and-click adventure. Melodies catchy and almost pop-structured, but no vocals. Sound palette entirely synthetic — warm analog pads, bright digital leads, punchy drum machine patterns.

Not straightforward synthwave or retro pastiche. Vaporwave sensibility — slightly uncanny, slightly melancholic quality from sounds that feel familiar but displaced. Sounds like a memory of something that never quite existed. Warm and inviting on the surface but emptiness underneath, like a shopping mall after hours.

Tempos vary — some upbeat/driving (the "chase scene"), others slow/contemplative (the "walking through a deserted lobby"). Rhythms quantized with mechanical precision contrasting the organic messiness of other genres.

### Instrumentation

- **Synthesizers (primary)**: warm analog pads (Juno-106, Jupiter-8 type), bright FM synthesis leads (DX7-style), plucky synth bass with filter sweeps. Curated 80s/early 90s synth presets, used with compositional sophistication.
- **Drum machines**: TR-808 or LinnDrum-style. Crisp, quantized, slight swing if any. Snappy snare, punchy kick, hissing hi-hat. Not aggressive — groove-oriented patterns, not bangers.
- **No guitars, no live drums, no acoustic instruments.** Absence of organic elements IS the point.
- **Occasional vocal samples** — not singing, but processed speech fragments, radio chatter, environmental recordings adding texture without narrative content.

### Production character

Clean and precise, but with deliberate analog character. Warmth in the low end — not digital thinness of actual MIDI files, but rounded, slightly saturated quality of sounds running through analog filters and converters. Mix relatively simple compared to dream pop — fewer layers, more space between elements. Each synth voice clearly audible with its own place in the frequency spectrum. Some tracks incorporate lo-fi — tape wobble, bit-reduction, slight VHS-quality to high end — as vaporwave nod. But seasoning, not main course.

### Mood / emotional vocabulary

Nostalgic, liminal, uncanny, retro-futuristic, wistful, synthetic, artificial warmth, after-hours, abandoned, cinematic, investigative, neon-lit-but-lonely. Driving through a small town at 3 AM where all the neon signs are still on but every building is closed. A VHS tape of a place that doesn't exist. The future as imagined from 1987 — optimistic but wrong. Loneliness disguised as aesthetic pleasure.

### Reference artists

Eyeliner (all albums), HOME, Tycho, Com Truise, Boards of Canada, Oneohtrix Point Never (early), Disasterpeace (It Follows soundtrack), Cliff Martinez (Drive soundtrack), Tangerine Dream, Vangelis (Blade Runner).

### Suno prompt keywords

`synthwave, vaporwave, retro synth, instrumental, MIDI pop, analog synthesizer, drum machine, 80s nostalgia, cinematic, atmospheric electronic, warm pads, FM synthesis, no vocals, ambient electronic, liminal, lo-fi synth, retro instrumental`

## Genre 6 — Blues-Punk / Dirty Rock

**Pacific Drive examples**: Leon Laudenbach ("Doctor Juice"); DADDY LONG LEGS ("Electro-Motive Blues"); Elliott BROOD ("Cadillac Dust").

### Sonic description

Greasy, sweaty outlier. Where garage-psych has punk energy fast and cathartic, blues-punk is slower, filthier, more groove-oriented. Mid-pace — a swaggering, hips-forward shuffle or stomp that makes you want to nod your head rather than thrash. Guitar tone absolutely central: thick crunchy overdrive with mids scooped, treble biting through. Cheap tube amp with a broken speaker — buzzes and rattles in an intentional, characterful way.

Vocal style raw and unpolished — snarl, howl, gravelly bark. Performative quality differs from the confessional intimacy of singer-songwriter. Vocalists are characters — showmen, storytellers, carnival barkers. Juke-joint blues shouters filtered through garage rock attitude.

### Instrumentation

- **Electric guitar**: overdriven, mid-heavy, thick woolly tone. Riffs simple and repetitive — two or three notes bent and hammered into submission. Slide guitar for extra slime. Grinds, buzzes, squawks rather than plays.
- **Bass**: locked with the kick drum, simple driving patterns. Picked for aggressive attack. Sometimes distorted.
- **Drums**: the shuffle is king. Kick-snare-kick-snare with hi-hat swing. Loose, behind the beat — not sloppy, relaxed for groove. Toms used for punctuation fills.
- **Harmonica**: optional but effective. Aggressive — heavy bends, overblown notes that wail.
- **Piano or organ**: honky-tonk piano or ratty organ providing rhythmic stabs on the off-beat.
- **Hand claps or tambourine**: primal human percussion layer.

### Production character

Deliberately lo-fi and raw. Live take in a small sweaty club — audible room sound, instruments bleeding into each other's mics, vocal slightly distorted from being too close to the mic. No reverb to speak of — dry and in-your-face. Mix front-loaded: everything competes in the midrange for a dense punchy wall of sound without much headroom. Should sound like meters are in the red.

### Mood / emotional vocabulary

Sleazy, swaggering, dangerous, humid, nocturnal, illicit, reckless, whiskey-soaked, predatory, electric, primal. Roadhouse bar at 1 AM — sticky floors, neon beer signs, someone's about to start a fight but the band just keeps playing. Muscle car with a bad exhaust idling in a parking lot. The sound of trouble walking through the door. In Old Growth: what plays when you find something in the woods that humans built and then abandoned — a still, a bunker, a camp that was left in a hurry.

### Reference artists

The Black Keys (early), R.L. Burnside, Junior Kimbrough, The White Stripes, Jon Spencer Blues Explosion, Hound Dog Taylor, Left Lane Cruiser, The Gun Club, The Cramps, Flat Duo Jets, André Williams.

### Suno prompt keywords

`blues punk, dirty blues, garage blues, slide guitar, lo-fi, shuffle beat, gravelly vocals, distorted, raw, mid-tempo, swampy, overdriven guitar, honky-tonk, roadhouse, sleazy, stomp, harmonica, primal rock`

## Genre 7 — Pagan Folk / Ritual Ambient

**Pacific Drive examples**: Gaia Consort ("Drawing Down the Moon"); Of The Wand And The Moon ("Whispers of the Past"); Patrik Jerlestam, Isabella Marison ("Disturbing the Quiet").

### Sonic description

Overlaps with dark folk/neofolk but distinctly different intent and texture. Where neofolk channels Southern Gothic and Americana, pagan folk draws from pre-Christian European and PNW indigenous-adjacent nature mysticism. Ancient and elemental — connected to the landscape itself rather than the humans moving through it.

Arrangements sparse and organic. Acoustic instruments dominate, played in unusual ways — bowed guitar, prepared strings, breath-like vocal textures that could be wind or voice. Percussion, when present, is hand drums (frame drum, bodhran, djembe) in slow processional rhythms that feel ceremonial.

Vocal style diverse: sometimes chanting or intoning (single sustained note or slowly oscillating melody), sometimes multi-tracked into layered choral texture. Lyrics, when present, may be indistinct or in archaic/invented language. Voice used more as instrument — source of tone and timbre — than vehicle for narrative.

### Instrumentation

- **Acoustic instruments played unconventionally**: bowed acoustic guitar, bowed psaltery, dulcimer, zither. Anything with strings that can sustain and drone.
- **Frame drums, bodhran, hand percussion**: slow, deliberate rhythms. Not polyrhythmic — simple, repetitive, almost metronomic patterns inducing trance.
- **Drone instruments**: hurdy-gurdy, harmonium, shruti box, or sustained synth pad. Continuous low tone providing harmonic foundation.
- **Flute or recorder**: wooden, breathy, pentatonic or modal melodies.
- **Field recordings**: wind, water, forest ambience, fire crackling. Not background — compositional elements, woven at equal volume with instruments.
- **Vocals**: chanting, overtone singing, wordless melodies, layered harmonics. Words, when present, delivered slowly with great weight — each syllable a separate invocation.

### Production character

Between studio recording and field recording. Instruments real and present — close-miked with natural room ambience — but field recordings and drone elements create a sense of enormous space, as if the music is being played in a forest clearing or a stone circle. Minimal processing: no compression, no limiting, natural dynamics. If a note is quiet, it is quiet. If a drum hit is loud, it is loud. The music breathes.

### Mood / emotional vocabulary

Sacred, primordial, ceremonial, ancient, elemental, cyclical, animistic, reverent, uncanny, moss-covered, rooted, deep-time. Standing in old-growth forest and understanding, viscerally, that the trees are older than your civilization. Mycelium networks. The turning of seasons as felt by something that doesn't measure time in years. Not threatening like neofolk — awe-inspiring. The forest as cathedral. The sublime in its original, terrifying sense.

### Reference artists

Dead Can Dance, Heilung (acoustic material), Wardruna, Current 93 (Of Ruine or Some Blazing Starre era), Comus, In Gowan Ring, Natural Snow Buildings, Earth (later drone work), Grouper, Eluvium.

### Suno prompt keywords

`pagan folk, ritual ambient, drone, frame drum, acoustic, chanting, field recordings, nature sounds, forest ambience, modal melody, ancient, ceremonial, hand percussion, bowed strings, harmonium, atmospheric, slow, processional, trance-inducing, earthy`

## Genre 8 — Dark Gospel / Sacred Americana

**Pacific Drive examples**: Mark Crawford, Kenny Lee Young ("Holy Mystery"); Sandy Salisbury ("The Sun is Shining Now").

### Sonic description

Smallest lane but arguably most emotionally potent when it appears. American sacred music — gospel, spirituals, hymns — filtered through darkness and isolation. Warmth and communal joy of traditional gospel replaced by something solitary and uncertain. One voice singing a hymn in an empty church, not a choir celebrating in a full one.

Arrangements built around voice and piano or organ, sometimes brass (trumpet, trombone) adding mournful sustained tones. Harmonic language traditional — major keys with gospel chord changes (I–IV–V with secondary dominants, minor substitutions, passing diminished chords) — but tempo and delivery drain them of their usual triumph. What's left: the raw ache at the core of all sacred music — the desire to be heard by something larger than yourself with no guarantee that anything is listening.

### Instrumentation

- **Piano**: upright, slightly out of tune. Played simply — block chords or slow rolling gospel pattern in the left hand with melody in the right. Not virtuosic — deliberate and weighty.
- **Organ**: not a church pipe organ — smaller, wheezier reed organ or a Hammond B3 with the Leslie on slow. Sustained chords swelling and receding like breathing.
- **Vocals**: single strong voice, untrained or intentionally unpolished. Conviction but not performance — singing alone, for themselves, maybe for God. Raw vibrato. Occasional vocal breaks or catches revealing emotion.
- **Brass**: trumpet playing a slow lonely countermelody. Muted trumpet is especially effective — sounds like someone crying through an instrument. Trombone for low warm sustain.
- **Hand claps or foot stomps**: a single person keeping time. Not a congregation — one body, one rhythm.
- **Upright bass**: bowed or plucked, deep woody foundation.

### Production character

Warm and close. Feels like a room — small church, living room, cabin. Close-miked vocals with natural room reverb (not added — the actual sound of the space). Piano should have character — not pristine Steinway but aged and personal, maybe a slightly buzzy string or a key that sticks. Analog warmth throughout. Mix vocal-forward — everything else serves the voice.

### Mood / emotional vocabulary

Reverent, aching, solitary, uncertain, yearning, humble, broken, hopeful-against-hope, devotional, human. Sunday morning after Saturday night — the hangover of the soul. Asking for forgiveness from something you're not sure exists. A candle guttering in a dark window. The last song before the radio signal dies. In Old Growth: what plays in the moment of calm before or after danger — the prayer before the descent, the gratitude after escape.

### Reference artists

Blind Willie Johnson, Sister Rosetta Tharpe, Sam Cooke (gospel), Mahalia Jackson, Sufjan Stevens (sacred), The Staple Singers, Sixteen Horsepower (hymn-adjacent), Howe Gelb, Richmond Fontaine, Bonnie Prince Billy.

### Suno prompt keywords

`dark gospel, sacred americana, piano, organ, solo voice, brass, trumpet, mournful, hymn, spiritual, lo-fi, intimate, reverb, slow tempo, devotional, raw vocals, upright bass, solitary, aching, broken gospel, americana`

## Production Unifier: The Radio Filter

Pacific Drive applies a lo-fi processing layer to all radio audio — a VHS/tape-style filter that degrades the signal and makes all genres feel cohesive through the shared medium of a crappy car speaker. For Old Growth, the biome-reactive signal degradation system serves this same function.

### What the filter does sonically

- High-frequency rolloff: reduces clarity and "air" above ~8 kHz, making everything sound warmer and duller
- Low-frequency reduction: thins out sub-bass, simulating small speaker limitations
- Subtle flutter/wow: very slight pitch modulation simulating tape or analog tuning instability
- Harmonic saturation: adds even harmonics in the midrange, simulating tube or tape warmth
- Noise floor: low-level hiss or static, varying with "signal strength"
- Narrow stereo field: collapses the mix toward mono, as a car radio would

### Implication for Suno generation

Generate tracks at full fidelity — the radio filter is applied in-engine at runtime, not baked into the audio files. This means source tracks can and should sound good on their own (for potential soundtrack release), but should also be designed to survive degradation gracefully. Tracks with important detail in the extreme high or low frequencies will lose that detail through the radio. Tracks that live in the midrange — where voice and guitar naturally sit — will come through the strongest.

## Target Genre Distribution for Old Growth

Based on Pacific Drive's ~70/20/10 ratio, adapted for a PNW old-growth forest survival context:

| Genre | Target % | Rationale |
|-------|----------|-----------|
| Haunted Americana / Gothic Singer-Songwriter | 30% | Emotional core. The "driving through the forest" sound. |
| Dark Folk / Neofolk | 20% | Builds dread and atmosphere. Essential for tension. |
| Dream Pop / Ethereal | 15% | Beauty and relief. Prevents the radio from being relentlessly dark. |
| Indie Rock / PNW Garage-Psych | 15% | Energy and catharsis. The "escape" music. |
| Pagan Folk / Ritual Ambient | 10% | Unique to Old Growth. The forest-as-entity sound. |
| Blues-Punk / Dirty Rock | 5% | Texture and surprise. The oddball that makes you pay attention. |
| Dark Gospel / Sacred Americana | 5% | Emotional punctuation. Rare but devastating when it appears. |

## Test Coverage

- `src/tests/unit/test_bus_radio.gd` — radio system integration
- `src/tests/unit/test_radio_channel.gd` — per-channel content handling

## Related

- [[Audio — Car Radio]] — the runtime that plays station content; channel roster + frequency assignments
- [[Audio — Pipeline]] — how station content is produced (TTS + post-processing)
- [[Audio — Forest Transmissions]] — what replaces station content after signal failure
- [[Narrative Direction]] — Gary and KTLK as narrative instruments
- [[Audio — Overview]] — top-level audio architecture
- [[Audio — Domain MOC]] — domain MOC
- [[Diegesis Thread]] — radio as diegetic commitment
ember's answer for note_placement-systems-audio-radio-stations, placementProbability ember gave each option.Art0.15Code0.01Decisions0.05Design0.63 ember's pickDiscoveries0.01Governance0.01Narrative0.04Reference0.05Sessions0.02Systems0.03 gold

note_placement-art-rock-scatter-design: placement

Note placement · test
Gold
Place the note under Art/ (Art)
ember
Place the note under Design/ (Design)
Signal
confidence 0.61 (verify band)
Kit outcome
acted, flagged for verification
Why this gold label
Mined from Art/Rock Scatter Design.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Rock Scatter Design

The design reference for scattering large rock formations throughout Old Growth's world — glacial erratics, outcrops, and boulders that break up the tree canopy, add visual variety, and reinforce biome identity. Particularly important for the Petrifold, where rocky terrain should dominate.

## Overview

Large rock formations follow the same architectural pattern as vegetation scatter: a scatter system that slots into the world generator pipeline, takes road curves + biome lookup + terrain as inputs, and places candidates via Poisson-disc rejection sampling. Rocks are sparser than trees (30–80m minimum spacing vs. tree spacing), larger on average, and biome-tinted.

## Design Intent

Rocks are not decoration. They participate in biome identity:

- In **Verdant Old Growth**, rocks are rare and small — an occasional lone erratic in a clearing, moss-toned. Scale-matched to a landscape dominated by massive trees.
- In **Fungal Depths**, rocks are moderate in density and mid-size — a transitional feel, purple-grey tinted to match the biome palette.
- In **Petrifold**, rocks are **dominant**. Large, frequent, pale stone. The biome's defining visual feature.

This gradient serves [[Biome Behavior]] — the same kind of entity (a rock) appearing differently across biomes because the biomes are three stages of the same process.

## Key Decisions

### Placement algorithm (Poisson-disc rejection sampling)

Same pattern as `vegetation_scatter.gd`:

1. Sample along road curves at regular intervals
2. Generate candidates in a distance band around the road (20–200m)
3. Reject candidates that fail any constraint:
   - **Road avoidance**: 15m+ clearance (rocks are large; the Samba must not clip)
   - **Spawn exclusion**: no rocks within 20m of vehicle spawn
   - **Terrain height**: skip NaN/Inf from `terrain.data.get_height()`
   - **Slope filtering**: erratics prefer flatter ground (< 15°); outcrops allowed steeper
   - **Minimum spacing**: 30–80m between rocks (wider than trees)
   - **Biome density**: probabilistic rejection via biome blend weights and biome multipliers
4. Place survivors with randomized rotation and scale

### Biome configuration (BIOME_CONFIG extension)

| Parameter | Verdant (0) | Fungal (1) | Petrifold (2) |
|---|---|---|---|
| `rock_density_multiplier` | 0.15 | 0.5 | 1.0 |
| `rock_scale_min` | 1.0 | 1.5 | 2.0 |
| `rock_scale_max` | 2.0 | 3.0 | 5.0 |

### Rock variants

Start with Godot primitives — deformed spheres/boxes with rocky materials — then swap for `.glb` models later.

| Variant | Base primitive | Aspect | Description |
|---|---|---|---|
| Erratic | Sphere | Tall, irregular | Large isolated boulder, partially embedded |
| Outcrop | Box | Wide, flat | Low rock shelf / formation |
| Boulder | Sphere | Round, squat | Medium rounded rock |
| Cluster | Box | Jagged, angular | Broken rock pile |

Per-instance randomization: Y rotation `rng.randf() * TAU`; scale-per-axis within variant range to break uniformity; ground embedding 20–40% below terrain height so rocks look planted.

### Material

`StandardMaterial3D`, biome-tinted albedo (mossy green Verdant, purple-grey Fungal, pale stone Petrifold), roughness 0.85–0.95, zero metallic, no emission.

### Collision strategy

Two strategies depending on final count:
- **Low count (< 50 per world)**: direct `StaticBody3D` per rock instance. Simple, no pooling overhead.
- **High count (50+)**: proximity-based collision pool (same pattern as `tree_collision_manager.gd`). Pre-allocate `StaticBody3D` nodes; activate within 100m of player, deactivate beyond 120m.

Collision shapes: `BoxShape3D` or `ConvexPolygonShape3D` scaled to rock bounds — not spheres.

### Rendering

`MultiMeshInstance3D` (same pattern as vegetation scatter): batch transforms per variant, apply biome material overrides. Keeps draw calls low with hundreds of rocks.

### Integration point

`world_generator.gd` Step 9, after vegetation scatter, before weather zone system. The full Step 9 order:
1. Fog system setup + valley fog placement
2. Vegetation scatter (trees, ferns, logs)
3. Tree collision manager initialization
4. **Rock scatter (erratics, outcrops, boulders)**
5. Weather zone system
6. Road structure scatter (buildings, props)

## How It Appears In-Game

In Verdant: an occasional large rock in a clearing catches amber light filtering through the canopy. In Fungal Depths: rocks form mid-ground silhouettes between bioluminescent mycelium and fog. In Petrifold: rocks dominate — the landscape reads as geological, and the calcified vehicles + calcified animals are a natural visual rhyme.

## Open Questions

> [!question] UNDOCUMENTED
> - Should erratics cast shadows at distance, or fade shadow at visibility range to save perf?
> - Should rock placement influence tree placement (clear trees near large rocks)?
> - Do rocks need LOD, or are primitives cheap enough to skip it?
> - Should Petrifold have unique rock coloring (e.g., petrified/crystalline look)?

## Related

- [[Biomes]] — the three biomes rocks visually reinforce
- [[Palettes]] — per-biome palette decisions
- [[Biome Behavior]] — cross-cutting biome thread
- [[World — Domain MOC]] — domain MOC
- [[Art]] — folder MOC
- [[Visual Direction]] — overall art direction
ember's answer for note_placement-art-rock-scatter-design, placementProbability ember gave each option.Art0.02 goldCode0.07Decisions0.05Design0.61 ember's pickDiscoveries0.02Governance0.01Narrative0.01Reference0.03Sessions0.01Systems0.17

note_placement-code-release-and-distribution: placement

Note placement · dev
Gold
Place the note under Code/ (Code)
ember
Place the note under Systems/ (Systems)
Signal
confidence 0.60 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Code/Release and Distribution.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Release and Distribution

The complete release and distribution pipeline for Old Growth — Apple Developer account setup end-to-end, macOS code signing and notarization, DMG packaging, Windows/Linux installer strategy, license bundling, and the full release workflow from version bump through GitHub Release publication. Consolidates three archived docs: `apple-developer-setup.md` (436 lines), `distribution-packaging-strategy.md` (429 lines), and `release-build-guide.md` (462 lines).

## Overview

Old Growth ships as a **macOS Universal Binary** and **Windows x86_64** via GitHub Releases. The pipeline is version-driven: bumping `config/version=` in `src/project.godot` and merging to `main` is the sole release trigger. The `release.yml` workflow detects the version change, builds platform artifacts in parallel on macOS and Windows runners, signs the macOS build with a Developer ID certificate, notarizes with Apple, packages both into their respective distribution formats, and publishes a single GitHub Release with both platform artifacts — no manual tagging, no manual upload.

See [[Build System]] for the Make-driven local workflow; this note is the release-specific pipeline plus platform distribution strategy.

## Quick Reference

```bash
# Validate pre-release
make all

# Bump version (current: check src/project.godot)
make bump-minor      # e.g. 0.6.0 → 0.7.0
make bump-patch      # e.g. 0.6.0 → 0.6.1
make bump-major      # e.g. 0.6.0 → 1.0.0

# Build
make build           # Ad-hoc signed .zip (fastest, for testing)
make dmg             # Ad-hoc signed .dmg (for local distribution testing)

# For CI-driven release (after pushing to main)
# → GitHub Actions handles everything automatically
```

## Platform Strategy

Currently Old Growth releases are distributed as **`OldGrowth-macOS.zip`** and `.dmg` — a simple bag of files and a drag-to-install DMG respectively. As the game approaches commercial release, platform-native installers improve user experience and follow industry standards:

| Format | UX | Installation | License Bundling |
|---|---|---|---|
| `.zip` | Manual extract/move | User-driven | Files in `.app` bundle only |
| `.dmg` (macOS, ✅ implemented) | Drag-to-install | Familiar | Can include LICENSES.md at top level |
| `.msi` (Windows, roadmap) | Wizard installer | Automated | Install dir includes docs |
| `.AppImage` (Linux, roadmap) | Single executable | "Just works" | Difficult to bundle docs |

macOS is the only shipping platform in v1.0 (per [[Tech Stack Review]] — scope constraint: macOS Universal Binary).

### Platform Prerequisites Summary

|  | macOS | Windows | Linux |
|---|---|---|---|
| Local tooling | Xcode CLT, Dev Program cert | Windows SDK, `.pfx` (optional) | `appimagetool` (optional) |
| CI secrets | 7 Apple secrets + keychain | 2 cert secrets (optional) | None |
| Signing mandatory? | Yes (Gatekeeper blocks unsigned) | No (SmartScreen warns only) | No |
| Biggest CI time sink | Notarization (2–5 min) | Export (~2 min) | Template download (~3 min) |

The only hard blocker before the first CI release is having the Apple Developer ID cert exported as a `.p12` and the app-specific password generated.

### macOS Prerequisites

**Locally (one-time)**:
- Xcode Command Line Tools: `xcode-select --install`
- Active Apple Developer Program membership
- Developer ID Application certificate in your keychain (Xcode > Settings > Accounts > Manage Certificates)
- App-specific password from appleid.apple.com
- Run `xcrun notarytool store-credentials` once to cache credentials locally

**In CI**:
- `macos-13` and `macos-latest` runners have Xcode pre-installed
- Secrets: `APPLE_CERT_P12_BASE64`, `APPLE_CERT_PASSWORD`, `KEYCHAIN_PASSWORD`, `APPLE_SIGN_IDENTITY`, `APPLE_ID`, `APPLE_TEAM_ID`, `APPLE_APP_PASSWORD`
- Export the `.p12` locally: Keychain Access > right-click Developer ID cert > Export > save as `.p12` > `base64 -i cert.p12 | pbcopy`

### Windows Prerequisites (roadmap)

**Locally (one-time)**:
- Windows SDK (for `signtool.exe`) — comes with Visual Studio or install standalone
- Code signing certificate as `.pfx` (optional for early dev):
  - Self-signed: free, suppresses warnings only on your machine
  - Authenticode cert from a CA (DigiCert, Sectigo, ~$200–500/yr): required to suppress SmartScreen for players

**In CI**:
- `windows-latest` runner has Windows SDK and `signtool.exe` pre-installed
- Secrets (optional): `WIN_CERT_PFX_BASE64`, `WIN_CERT_PASSWORD`
- Export locally: `base64 -i cert.pfx | pbcopy` (macOS) or `[Convert]::ToBase64String([IO.File]::ReadAllBytes('cert.pfx')) | clip` (Windows)
- For early direct downloads, signing can be skipped entirely. SmartScreen shows "unknown publisher" but players can click through. Add the cert before Steam.

### Linux Prerequisites (roadmap)

**Locally**:
- No signing obligation on Linux
- `appimagetool` for testing AppImage packaging locally (optional)
- GPG key for signed releases (optional): `gpg --gen-key`

**In CI**:
- `ubuntu-latest` needs `libfuse2` at runtime (one `apt-get` line)
- `appimagetool` downloaded automatically by packaging script if not present
- No secrets needed unless GPG signing is added

### Godot Export Templates (all platforms)

**Locally**:
- In Godot: Editor > Manage Export Templates > Download for your exact version
- Set up export presets (Project > Export) for each platform and commit `export_presets.cfg`

**In CI**:
- Export template caching is configured via `actions/cache@v4` keyed on `GODOT_VERSION`
- Templates are only downloaded on cache miss (~1 GB, adds ~2–3 min)

## One-Time Setup (Development Environment)

```bash
# Rust stable
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

# Godot 4.6
# Download from https://godotengine.org/download
# Install to /Applications/Godot_mono.app (or adjust GODOT_APP in Makefile)

# Add Godot to PATH
make godot-path
source ~/.zshrc

# Python venv + dependencies
make setup

# Submodules (assets)
make submodule-init
```

Verify setup:

```bash
# All commands should succeed
godot --version
cargo --version
python3 --version
git --version
```

## Apple Developer Setup — Full Walkthrough

### Part 1 — Apple Developer Program Membership

#### Step 1.1 — Enroll

**URL**: https://developer.apple.com/enroll/

1. Click **Enroll**
2. Sign in with your Apple ID (create one at appleid.apple.com if needed)
3. Choose enrollment type:
   - **Individual**: $99/year, for personal development
   - **Organization**: $99/year, requires legal entity verification
4. Agree to Apple Developer Program License Agreement
5. Complete enrollment (payment via credit card)
6. Wait for Apple to verify (can take 1–2 business days for organizations)

#### Step 1.2 — Verify Your Account

Once enrolled:

1. Go to **https://developer.apple.com/account/**
2. Sign in with your Apple ID
3. Under **Membership**, you should see:
   - Status: **Active**
   - Type: **Individual** or **Organization**
   - Renewal date: one year from enrollment

**You're now a registered Apple developer.**

### Part 2 — Developer ID Certificate

#### Step 2.1 — Create Certificate Signing Request Locally

Developer ID certificates are used to sign apps for distribution outside the Mac App Store.

**On your Mac**:

1. Open **Keychain Access** (Applications > Utilities > Keychain Access)
2. Menu: **Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority**
3. Fill out:
   - **User Email Address**: your Apple ID email
   - **Common Name**: your name or organization name (e.g., "Josh Burt")
   - **CA Email Address**: leave blank
   - **Request is**: select **Saved to disk**
4. Click **Continue**
5. Save the request as `CertificateSigningRequest.certSigningRequest` to your Desktop

#### Step 2.2 — Request Developer ID Certificate from Apple

**On https://developer.apple.com/account/**:

1. Go to **Certificates, Identifiers & Profiles > Certificates**
2. Click the **+** button to create a new certificate
3. Under **Development**, select **Developer ID Application** (this is for distribution outside the Mac App Store)
4. Click **Continue**
5. Follow the prompts:
   - Upload the `CertificateSigningRequest.certSigningRequest` file from Step 2.1
   - Click **Continue**
6. Apple generates your certificate (usually instant)
7. Click **Download** to save `DeveloperIDApplication.cer` to your Desktop

#### Step 2.3 — Install Certificate in Keychain

**On your Mac**:

1. In Keychain Access, go to **Certificates** category
2. Double-click `DeveloperIDApplication.cer` (from Step 2.2)
3. A dialog appears asking to confirm installation
4. Click **Add** to install into your login keychain
5. Keychain Access now shows your Developer ID certificate:
   - Name: **Developer ID Application: [Your Name] ([TEAM_ID])**
   - Type: **Certificate**
   - Expires: 3 years from issuance

**Verify installation**:

```bash
security find-identity -v -p codesigning | grep "Developer ID"
```

Should output:

```
1) XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX "Developer ID Application: Josh Burt (A1B2C3D4E5)"
```

(The long string is your **Signing Identity**.)

### Part 3 — App-Specific Password (for Notarization)

Apple requires an **app-specific password** (not your main Apple ID password) for the notarization API.

#### Step 3.1 — Generate App-Specific Password

**On https://appleid.apple.com/**:

1. Sign in with your Apple ID
2. Go to **Security** section
3. Under **App-Specific Passwords**, click **Create password** (or the **+** button)
4. Select:
   - **App**: Other (Custom)
   - **Device**: Mac
5. A generated password appears (e.g., `xxxx-xxxx-xxxx-xxxx`)
6. **Copy and save this password somewhere safe** (you'll need it for CI/CD and local builds)

**Important**:
- This is NOT your Apple ID password
- You'll use it for the `APPLE_APP_PASSWORD` environment variable
- It only works with the notarization API, not for signing in anywhere

### Part 4 — Export Developer ID Certificate as `.p12`

For CI/CD (GitHub Actions), you need to export your certificate as a `.p12` file and encode it as base64.

#### Step 4.1 — Export Certificate from Keychain

**On your Mac**:

1. Open **Keychain Access**
2. In the **Certificates** category, find:
   - **Developer ID Application: [Your Name] ([TEAM_ID])**
3. Right-click on it and select **Export "[Your Name]"**
4. Save as `DeveloperIDCert.p12` to your Desktop
5. A password dialog appears — **set a strong password** (you'll use this for GitHub Secrets)
6. Click **Save**

#### Step 4.2 — Verify Export

```bash
# Check that the .p12 file exists and is valid
file ~/Desktop/DeveloperIDCert.p12
# Should show: "~/Desktop/DeveloperIDCert.p12: data"

# Verify it contains the certificate
security find-certificate -a -c "Developer ID" ~/Desktop/DeveloperIDCert.p12 2>/dev/null && echo "✓ Certificate found"
```

### Part 5 — Gather Your Credentials

You now have everything needed for code signing and notarization. Gather these values:

```
Apple ID Email:              your-apple-id@example.com
Team ID:                     A1B2C3D4E5  (visible in certificate name)
Signing Identity:            Developer ID Application: Josh Burt (A1B2C3D4E5)
Developer ID Certificate:    ~/Desktop/DeveloperIDCert.p12
Certificate Password:        [the password you set in Step 4.1]
App-Specific Password:       xxxx-xxxx-xxxx-xxxx
```

**Find Team ID if you can't find it in the certificate name**:

```bash
# List all Developer ID certificates and their Team ID
security find-identity -v -p codesigning
# Look for the line with "Developer ID" and extract the (TEAM_ID) part
```

### Part 6 — Local Build Environment

#### Step 6.1 — Set Environment Variables

For local builds with Developer ID signing (not ad-hoc), export these:

```bash
export APPLE_SIGN_IDENTITY="Developer ID Application: Josh Burt (A1B2C3D4E5)"
export APPLE_ID="your-apple-id@example.com"
export APPLE_TEAM_ID="A1B2C3D4E5"
export APPLE_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx"
```

**Store these permanently** (optional but recommended). Add to your `~/.zshrc` (or `~/.bashrc`):

```bash
# Old Growth — Apple Developer Credentials
export APPLE_SIGN_IDENTITY="Developer ID Application: Josh Burt (A1B2C3D4E5)"
export APPLE_ID="your-apple-id@example.com"
export APPLE_TEAM_ID="A1B2C3D4E5"
export APPLE_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx"
```

Then:

```bash
source ~/.zshrc
```

#### Step 6.2 — Verify Local Signing

Test that you can sign locally:

```bash
# Build an app (ad-hoc signed)
make build

# Now sign with Developer ID
bash scripts/ci/sign_dmg.sh \
  --app "build/Old Growth.app" \
  --sign "$APPLE_SIGN_IDENTITY" \
  --apple-id "$APPLE_ID" \
  --team-id "$APPLE_TEAM_ID" \
  --password "$APPLE_APP_PASSWORD" \
  --output "build/OldGrowth-macOS-test.dmg"
```

If successful, you should see:

```
✓ Notarization successful
✓ Notarization stapled
✓ DMG verified by Gatekeeper
```

### Part 7 — GitHub Repository Secrets (for CI/CD)

#### Step 7.1 — Convert `.p12` to Base64

Before setting up GitHub secrets, encode your certificate:

```bash
base64 -i ~/Desktop/DeveloperIDCert.p12 | pbcopy
# Copies the base64-encoded certificate to your clipboard
```

Or save to a file:

```bash
base64 -i ~/Desktop/DeveloperIDCert.p12 > ~/Desktop/cert-base64.txt
```

#### Step 7.2 — Add GitHub Secrets

**On GitHub** (https://github.com/shapeandshare/oldgrowth/settings/secrets/actions):

1. Click **New repository secret**
2. Add each of these (values from Part 5):

| Secret Name | Value |
|---|---|
| `APPLE_CERT_P12_BASE64` | Base64-encoded `.p12` (from Step 7.1) |
| `APPLE_CERT_PASSWORD` | Password you set when exporting `.p12` |
| `KEYCHAIN_PASSWORD` | Any random string (e.g., `TempKeychain123!`) |
| `APPLE_SIGN_IDENTITY` | `Developer ID Application: Josh Burt (A1B2C3D4E5)` |
| `APPLE_ID` | `your-apple-id@example.com` |
| `APPLE_TEAM_ID` | `A1B2C3D4E5` |
| `APPLE_APP_PASSWORD` | `xxxx-xxxx-xxxx-xxxx` (app-specific password) |

**Also add (if not already present)**:

| Secret Name | Value |
|---|---|

#### Step 7.3 — Verify Secrets

```bash
# From the repo, list secrets (names only, not values)
gh secret list
```

All 7 Apple secrets should appear.

### Part 8 — Pre-Release Checklist

Before your first release:

- [ ] Enrolled in Apple Developer Program (status: Active)
- [ ] Developer ID Application certificate installed in Keychain
- [ ] App-specific password generated at appleid.apple.com
- [ ] `.p12` certificate exported and password verified
- [ ] All 7 Apple secrets added to GitHub repository
- [ ] Local environment variables set (optional but recommended)
- [ ] Tested local signing: `make build && bash scripts/ci/sign_dmg.sh ...`
- [ ] Gatekeeper approved the locally-signed DMG (`spctl --assess ...`)
- [ ] Version bump tested locally: `make bump-minor && git log -1`
- [ ] Ready to push to main and let CI handle the release

## Release Workflow

### Phase 1 — Pre-Release Validation (local)

```bash
# Full CI pipeline — mimics GitHub Actions
make all
```

This runs (stops on first failure):

- `make semver` — component version enforcement
- `make game-version-check` — game version SemVer validation
- `make gdignore-check` — asset safety (`.gdignore` checks)
- `make gdscript-quality` — GDScript formatting + linting
- `make rust-test` — Rust unit tests
- `make rust-clippy` — Rust linting with strict warnings
- `make test` — GUT unit tests (headless)
- `make headless` — Godot project sanity check
- `make export-smoke` — quick export test

**If all pass, you're cleared to build.**

### Phase 2 — Version Bump

The game version lives **exclusively** in `src/project.godot` (`config/version=`).

#### Bump Options

```bash
# Patch: bug fix or hot fix
make bump-patch      # 0.6.0 → 0.6.1

# Minor: feature complete, new phase shipped
make bump-minor      # 0.6.0 → 0.7.0

# Major: epic complete (e.g., 8-phase release)
make bump-major      # 0.6.0 → 1.0.0

# Explicit version
./scripts/release/bump_version.sh 0.7.0
```

#### What the Bump Does

1. Reads current version from `src/project.godot`
2. Computes new SemVer
3. Writes new version back to `src/project.godot`
4. **Creates a git commit** with message: `chore: bump version X.Y.Z -> A.B.C`

#### Verify the Bump

```bash
# Check the version was updated
grep 'config/version=' src/project.godot

# Check the commit was created
git log -1 --oneline
# Should show: "chore: bump version 0.6.0 -> 0.7.0"
```

#### Article III Rule 15 — Bump Convention

- **PATCH** `X.Y.Z+1` — bug fixes, config refactors, hardening (no new gameplay)
- **MINOR** `X.Y+1.0` — feature complete, completed phase spec (new gameplay capability)
- **MAJOR** `X+1.0.0` — epic complete (e.g., phases 1–8)

AI agents MUST run `make bump-*` as part of any PR that touches `src/**` or `rust/**`. The level is a human decision for ambiguous cases; agents apply the bump on the feature branch when scope is unambiguous.

### Phase 3 — Build Release Binary

Three options covering testing through production:

#### Option A — Ad-Hoc Signed (fast, local testing)

**Best for**: testing the build process, local distribution.

```bash
make build
```

**Output**:
- `build/OldGrowth-macOS.zip` — compressed signed app bundle
- `build/Old Growth.app` — signed app (retained for inspection)

**Time**: ~10–15 minutes.

**What happens**:
1. Compiles Rust in release mode (Universal Binary: arm64 + x86_64)
2. Exports Godot project
3. Syncs version to `export_presets.cfg`
4. Ad-hoc signs all Mach-O binaries and app bundle
5. Creates `.zip` archive

**Verify the build**:

```bash
# Extract and test
unzip build/OldGrowth-macOS.zip -d /tmp/test
open "/tmp/test/Old Growth.app"

# Check architecture (should show both arm64 and x86_64)
file "/tmp/test/Old Growth.app/Contents/MacOS/Old Growth"
lipo -info "/tmp/test/Old Growth.app/Contents/MacOS/Old Growth"

# Verify signature
codesign --verify --deep --strict "/tmp/test/Old Growth.app"
codesign -vvv "/tmp/test/Old Growth.app"
```

#### Option B — Ad-Hoc DMG (installer, local testing)

**Best for**: testing the installer experience locally without Apple credentials.

```bash
make dmg
```

**Output**:
- `build/OldGrowth-macOS-0.7.0.dmg` — installer disk image (version-stamped)

**Time**: ~15–20 minutes total.

**What happens**:
1. Runs `make build` (see above)
2. Creates staging directory with app + `/Applications` symlink
3. Packages as UDZO-compressed `.dmg`
4. Ad-hoc signs the DMG

**Test the installer**:

```bash
# Open the DMG
open "build/OldGrowth-macOS-0.7.0.dmg"

# User drags "Old Growth.app" to "Applications" folder
# Then opens from Applications
# Verify everything works

# Check DMG signature
codesign --verify "build/OldGrowth-macOS-0.7.0.dmg"
```

#### Option C — Developer ID + Notarization (production)

**Best for**: production releases (requires Apple Developer Program membership).

**Prerequisites**:
- Developer ID Application certificate in keychain
- Apple ID + team ID
- App-specific password (not your main Apple ID password)
- Active Apple Developer Program membership

**Setup**:

```bash
# Export your Developer ID certificate as .p12
# 1. Keychain Access > Select your Developer ID certificate
# 2. Right-click > Export "[Your Name] (TEAM_ID)"
# 3. Save as "cert.p12"
# 4. Convert to base64:
base64 -i ~/Desktop/cert.p12 | pbcopy

# Then set these environment variables:
export APPLE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
export APPLE_ID="your-apple-id@example.com"
export APPLE_TEAM_ID="XXXXXXXXXX"
export APPLE_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx"
# ^ NOT your main Apple ID password — get from appleid.apple.com
```

**Run the full signing process**:

```bash
# Make sure you have a built app first
make build

# Then sign with Developer ID and notarize
bash scripts/ci/sign_dmg.sh \
  --app "build/Old Growth.app" \
  --sign "$APPLE_SIGN_IDENTITY" \
  --apple-id "$APPLE_ID" \
  --team-id "$APPLE_TEAM_ID" \
  --password "$APPLE_APP_PASSWORD" \
  --output "build/OldGrowth-macOS-0.7.0.dmg"
```

**What happens**:
1. Signs all nested Mach-O binaries with Developer ID + timestamp
2. Signs app bundle with entitlements + hardened runtime
3. Creates DMG
4. Signs DMG with timestamp
5. **Submits to Apple for notarization** (waits for approval, ~5–10 min usually)
6. **Staples notarization ticket** to DMG
7. Verifies with Gatekeeper (`spctl`)

**Time**: ~20–30 minutes (including notarization wait).

**Output**:
- `build/OldGrowth-macOS-0.7.0.dmg` — production-ready, Gatekeeper-verified

**Verify notarization**:

```bash
# Check Gatekeeper approval
spctl --assess --type open --context context:primary-signature "build/OldGrowth-macOS-0.7.0.dmg"
# Should print: "accepted"

# Verify signature details
codesign -vvv "build/OldGrowth-macOS-0.7.0.dmg"

# Check notarization staple
xcrun stapler validate "build/OldGrowth-macOS-0.7.0.dmg"
```

### Phase 4 — Verification Checklist

Before shipping, verify the build:

```bash
# ✓ Signature is valid
codesign --verify --deep --strict "build/Old Growth.app"

# ✓ Universal binary (both architectures present)
lipo -info "build/Old Growth.app/Contents/MacOS/Old Growth"
# Should output: "Mach-O universal binary with 2 architectures: [arm64:x86_64]"

# ✓ All Mach-O files are signed
file "build/Old Growth.app/Contents/MacOS/Old Growth"
# Should show "Mach-O 64-bit universal binary"

# ✓ DMG can be opened
open "build/OldGrowth-macOS-0.7.0.dmg"
# Should mount and show "Old Growth.app" + "Applications" symlink

# ✓ App can be extracted and launched from DMG
# (Mount DMG, drag to Applications, launch from Applications)

# ✓ Game starts without crashes (check logs)
open ~/Library/Application\ Support/Godot/app_userdata/Old\ Growth/output.log
```

### Phase 5 — Production Release (GitHub Actions)

When you're ready to release to players:

#### Step 1 — Ensure Version is Bumped

```bash
git log -1 --oneline
# Should show version bump commit
```

#### Step 2 — Push to Main

```bash
git push origin main
```

#### Step 3 — GitHub Actions Takes Over

The workflow `.github/workflows/release.yml` automatically:

1. **Detects version change** in `src/project.godot`
2. **Exports x86_64 app** on `macos-13` runner
3. **Exports arm64 app** on `macos-latest` runner
4. **Creates universal binary** from both architectures
5. **Signs with Developer ID** (from GitHub Secrets)
6. **Submits for notarization** (waits for Apple approval)
7. **Staples notarization ticket**
8. **Creates `.dmg` installer**
9. **Publishes GitHub Release** with `.dmg` asset:
   - URL: `github.com/shapeandshare/oldgrowth/releases/tag/vX.Y.Z`
   - Asset: `OldGrowth-macOS-X.Y.Z.dmg`

**You don't need to do anything else.** The workflow handles signing, notarization, and release publication.

#### Monitor the Workflow

```bash
# Watch the workflow in real-time
# https://github.com/shapeandshare/oldgrowth/actions

# Or check locally
gh run list --workflow=release.yml --limit=1
gh run view <run-id> --log  # View detailed logs
```

### CI Workflow Suppression — Known Failure Mode

**GitHub suppresses `push`-based workflow triggers when a merge commit modifies `.github/workflows/` files.** Platform security constraint, not a bug.

**Symptom**: PR merges to main; no Release workflow runs appear.

**When it happens**: PR contains both workflow file changes AND game-affecting files in the same merge commit.

**Prevention**: keep workflow changes in dedicated PRs that don't touch `src/` or `rust/`. Merge the workflow PR first, then merge the version bump PR separately.

**Recovery**: Actions → Release workflow → Run workflow → `main`.

## macOS `.dmg` Pipeline Detail

### Pipeline Overview

The release workflow (`.github/workflows/release.yml`) triggers on version bump to `main`:

1. **Job 1** (`check-version`): detects version bump in `src/project.godot`
2. **Job 2** (`export-x86`): builds x86_64 `.app` on Intel runner (`macos-13`)
3. **Job 3** (`export-arm`): builds arm64 `.app` on ARM runner (`macos-latest`)
4. **Job 4** (`sign-and-release`): merges into universal binary, signs with Developer ID, notarizes, creates `.dmg`, publishes GitHub Release

### Scripts

| Script | Purpose |
|---|---|
| `scripts/ci/make_universal.sh` | Combines x86_64 + arm64 `.app` bundles via `lipo` |
| `scripts/ci/sign_dmg.sh` | Codesigns (inside-out), creates DMG, notarizes, staples |
| `scripts/ci/create_dmg.sh` | Legacy — simple DMG creation without signing (kept for reference) |

### Local Development

```bash
make dmg    # Builds the game, then creates an ad-hoc signed .dmg
```

### Required GitHub Secrets

| Secret | Purpose |
|---|---|
| `APPLE_CERT_P12_BASE64` | Developer ID certificate (base64-encoded `.p12`) |
| `APPLE_CERT_PASSWORD` | Password for the `.p12` certificate |
| `KEYCHAIN_PASSWORD` | Temp keychain password (any random string) |
| `APPLE_SIGN_IDENTITY` | e.g. `"Developer ID Application: Name (TEAM_ID)"` |
| `APPLE_ID` | Apple ID email for notarization |
| `APPLE_TEAM_ID` | Apple Developer Team ID |
| `APPLE_APP_PASSWORD` | App-specific password for notarization |

### macOS `.dmg` Checklist

- [x] Create `scripts/ci/create_dmg.sh`
- [x] Create `scripts/ci/sign_dmg.sh` (codesign + notarize + DMG)
- [x] Create `scripts/ci/make_universal.sh` (universal binary merger)
- [x] Update `release.yml` to build `.dmg` with parallel arch exports
- [x] Test `.dmg` creation locally
- [x] Update `Makefile` with `dmg` target
- [ ] Set up Apple Developer ID secrets in GitHub
- [ ] Test full CI pipeline end-to-end
- [ ] Test on actual macOS machines before shipping

## Windows `.msi` — Roadmap

Windows Installer (`.msi`) is the standard for Windows software distribution. WiX Toolset is the industry standard.

### Benefits

- ✅ Familiar to Windows users — expected installer format
- ✅ Automated installation — users don't manually move files
- ✅ Start menu shortcuts & app registry integration
- ✅ Uninstaller included
- ✅ Easy to bundle docs — installer can place files in Program Files

### Implementation Options

#### Option A — WiX Toolset (recommended for long-term)

**WiX** (Windows Installer XML) is the industry standard. Generates professional `.msi` files.

```
Tools needed:
- Windows build machine (GitHub Actions: windows-latest)
- WiX Toolset 3.x (open source)
- Heat.exe (artifact harvesting)
```

Example workflow outline:

```yaml
# .github/workflows/release.yml - add Windows job
build-windows:
  name: Build Windows Installer (.msi)
  runs-on: windows-latest
  if: needs.check-version.outputs.version_changed == 'true'
  steps:
    - uses: actions/checkout@v4

    - name: Install WiX Toolset
      run: |
        choco install wixtoolset -y

    - name: Export Godot project
      run: |
        # ... godot export to build/ci-export/oldgrowth-game.exe

    - name: Generate WiX source from artifacts
      run: |
        # Heat.exe to scan oldgrowth-game.exe, LICENSES.md, etc.
        heat dir build/ci-export -o artifacts.wxs -t ...\Product.xsl

    - name: Build .msi
      run: |
        candle.exe Product.wxs artifacts.wxs -o build/
        light.exe build/*.wixobj -out OldGrowth-Windows-${VERSION}.msi

    - name: Upload to GitHub Release
      # ... attach .msi to release
```

#### Option B — NSIS (quick alternative)

**NSIS** (Nullsoft Scriptable Install System) is simpler to set up if you want to get Windows support faster.

- Pros: lightweight, quick setup, no commercial licensing
- Cons: less polished UX than `.msi`, harder to customize

#### Option C — Inno Setup (another alternative)

Similar to NSIS, slightly better UX.

### Recommended Path for Old Growth

1. **Start with macOS (`.dmg`)** — easier, you have macOS CI already
2. **Add Windows support when shipping is imminent**
3. **Use WiX if you want professional appearance; NSIS if you want speed**

### Key Considerations for Windows

- **Code signing**: sign the `.exe` and `.msi` with an EV certificate (prevents "Unknown Publisher" warning)
  - Cost: ~$200/year for EV certificate
  - Can be added to GitHub Secrets
- **Install location**: `C:\Program Files\Old Growth\` (standard)
- **License bundling**: place `LICENSES.md` in install directory so it's easily accessible

### Windows `.msi` Checklist (future)

- [ ] Decide on WiX vs NSIS vs Inno
- [ ] Create installer script (`.wxs` / `.nsi` / `.iss`)
- [ ] Add Windows build job to `release.yml`
- [ ] Obtain EV code signing certificate
- [ ] Test installer on Windows 10/11 machines
- [ ] Update release notes template

## Linux `.AppImage` or `.deb` — Roadmap

### Why Linux is Lower Priority

- Old Growth targets macOS primarily (per AGENTS.md: "Current development target is macOS")
- Linux support can come after macOS/`.msi` are stable
- Godot + Rust on Linux is well-supported, but distribution varies by distro

### When You're Ready

**Option A — `.AppImage`** (universal, easiest):
- Single executable that works on any Linux distro
- No dependencies to install
- Similar user experience to "just download and run"
- Tools: `linuxdeploy-x86_64.AppImage`

**Option B — `.deb`** (Debian/Ubuntu-specific):
- Standard for Ubuntu/Debian systems
- Requires package repository or manual download
- Better integration with apt package manager
- Tools: `fpm` (Effing Package Manager)

**Recommendation**: start with `.AppImage` (universal), add `.deb` later if you want Ubuntu-specific support.

## License Bundling Strategy

### Current State

- Licenses in `src/addons/*/LICENSE*` → **packed into `.app` bundle**
- `LICENSES.md` at repo root → **NOT in binary**
- Users must explore `.app` internals to find licenses

### Post-Migration State (macOS `.dmg`)

```
OldGrowth-macOS-X.Y.Z.dmg
├── Old Growth.app/
│   ├── Contents/Resources/addons/*/LICENSE    (from binary)
│   └── ...
└── Applications/ → /Applications/              (symlink, for drag-to-install)
```

**Licenses are still accessible** inside the `.app` bundle via:
1. Right-click **Old Growth.app** → **Show Package Contents**
2. Navigate to `Contents/Resources/addons/*/LICENSE`

**Or** users can consult the project repository: `github.com/shapeandshare/oldgrowth/blob/main/LICENSES.md`

**Why this approach is fine**:
- MIT licenses require the license file be "included in all copies" — ✅ it's in the `.app`
- Users who need licenses can find them (not user-unfriendly, just not automatically visible)
- `.dmg` stays minimal and clean (no extra clutter)

### Windows (`.msi`) Layout

```
C:\Program Files\Old Growth\
├── oldgrowth-game.exe
├── addons/*/LICENSE              (from binary)
└── LICENSES.md                   (placed by installer, optional)
```

**User experience**: installer creates Start Menu shortcut, can optionally include `LICENSES.md` in install dir.

### Linux (`.AppImage`) Layout

```
OldGrowth-X.Y.Z.x86_64.AppImage
└── (self-contained binary)

Licenses inside binary, or LICENSES.md on release page
```

## Migration Checklist

### Phase 1 — macOS (`.dmg`) — ✅ IMPLEMENTED

- [x] Create signing and DMG scripts
- [x] Update `release.yml` with full pipeline
- [x] Test `.dmg` creation locally
- [ ] Set up Apple Developer ID secrets in GitHub
- [ ] Test full CI pipeline end-to-end
- [ ] Release next version with `.dmg`

### Phase 2 — Windows (`.msi`) — later

- [ ] Decide on installer tool (WiX recommended)
- [ ] Create installer script template
- [ ] Add Windows build job to CI
- [ ] Obtain code signing certificate (if desired)
- [ ] Test on Windows 10/11 VMs
- [ ] Document installer behavior for support

### Phase 3 — Linux (`.AppImage`) — later

- [ ] Create `.AppImage` build script
- [ ] Add Linux build job to CI
- [ ] Test on multiple Linux distros
- [ ] (Optional) create `.deb` package for Ubuntu/Debian

## Risk Mitigation

### What Could Go Wrong?

| Risk | Mitigation |
|---|---|
| `.dmg` won't notarize | Check Apple's requirements; test early with test account |
| Users can't figure out drag-to-install | Include video walkthrough in release notes |
| `.msi` installer gets flagged by antivirus | Use proper code signing certificate; test with VirusTotal |
| Linux `.AppImage` doesn't work on some distros | Start with `.AppImage`, add `.deb` support based on user requests |
| `LICENSES.md` gets out of sync | Automate: regenerate from script before each build |

### Graceful Fallback

If any platform installer fails to build, the CI/CD should:

1. ✅ Still build the binary (`Old Growth.app` / `.exe` / binary)
2. ⚠ Warn that installer failed
3. 📦 Provide fallback `.zip` download

```bash
# In release.yml, wrap installer creation in try/catch:
- name: Create .dmg (may fail)
  continue-on-error: true
  id: dmg
  run: scripts/ci/create_dmg.sh

- name: Create fallback .zip if .dmg failed
  if: steps.dmg.outcome == 'failure'
  run: zip -r OldGrowth-macOS-fallback.zip build/ci-export/Old\ Growth.app
```

## Timeline & Effort Estimate

| Phase | Task | Effort | Timeline |
|---|---|---|---|
| macOS `.dmg` | Script creation & testing | 4–6 hours | 1 week |
| macOS `.dmg` | CI/CD integration | 2–3 hours | 1 week |
| macOS `.dmg` | Notarization setup (optional) | 1–2 hours | 1 week |
| Windows `.msi` | Installer tool setup & scripting | 8–12 hours | 2–3 weeks |
| Windows `.msi` | CI/CD + code signing | 4–6 hours | 2–3 weeks |
| Linux `.AppImage` | Build script creation | 4–6 hours | 1–2 weeks |
| Linux `.deb` | Package creation | 4–8 hours | 1–2 weeks (optional) |

**Recommended order**: macOS → Windows → Linux (based on target platforms).

## Troubleshooting

### Build Fails

| Symptom | Cause | Fix |
|---|---|---|
| `godot` not found | PATH not updated | `make godot-path && source ~/.zshrc` |
| Rust build fails | Toolchain outdated | `rustup update stable` |
| Export fails | Godot cache corrupted | `make godot-cache-clean`, restart Godot |
| "cannot find dylib" | Rust dylib not copied | Run `make rust-build` first |

### Signing Fails

| Symptom | Cause | Fix |
|---|---|---|
| `codesign: invalid active developer path` | Xcode Command Line Tools missing | `xcode-select --install` |
| `codesign: signing failed` | No valid signing identity | Check `security find-identity -v -p codesigning` |
| `codesign: unknown error` | Entitlements file invalid | Check syntax, regenerate from `sign_dmg.sh` |

### DMG / Notarization Issues

| Symptom | Cause | Fix |
|---|---|---|
| Notarization times out | Network issue or Apple busy | Retry: `xcrun notarytool history --apple-id <id>` |
| Notarization rejected | Code issues (hardened runtime incompatibility) | Check Apple's rejection log in `sign_dmg.sh` output |
| DMG won't mount | Creation failed | Check `hdiutil` output, free disk space |
| "damaged and can't be opened" | Code signature is invalid | Rebuild with `make build` |

### Certificate Issues

| Problem | Solution |
|---|---|
| `codesign: invalid active developer path` | Run `xcode-select --install` to install Command Line Tools |
| `code signing identity not found` | Verify certificate is in Keychain: `security find-identity -v -p codesigning` |
| `cannot find certificate in .p12` | Re-export from Keychain, verify password is correct |
| `certificate has expired` | Developer ID certs expire after 3 years. Request a new one from developer.apple.com |

### Notarization Issues

| Problem | Solution |
|---|---|
| `Notarization failed` | Check `sign_dmg.sh` output for Apple's rejection log. Common issues: hardened runtime incompatibility, entitlements problems, code signature invalid. |
| `invalid credentials for Apple ID` | Verify `APPLE_APP_PASSWORD` is an app-specific password (from appleid.apple.com), NOT your main Apple ID password |
| `Notarization times out` | Network issue or Apple's servers are busy. Retry: `xcrun notarytool history --apple-id <id>` to check status |
| `Stapler failed` | Notarization submission failed. Check the log for rejection reason and fix before retrying. |
| `Gatekeeper rejection after stapling` | Signature verification failed. Rebuild with `make build` and re-sign. |

### Password / Credential Issues

| Problem | Solution |
|---|---|
| `xcrun notarytool: error: The app-specific password is incorrect` | Double-check the app-specific password from appleid.apple.com. Make sure it's NOT your main Apple ID password. |
| `security: SecKeychainItemCopyContent: The user name or passphrase you entered is not correct.` | The password you set when exporting `.p12` is wrong. Re-export with the correct password. |
| `GitHub Actions secret masked in logs` | This is normal and expected — secrets are redacted for security. |

### GitHub Actions Failures

| Problem | Solution |
|---|---|
| `Release job skipped` | Check if version in `src/project.godot` actually changed from the previous commit. The workflow only runs if version changes. |
| `Export job times out` | Godot export can take 20+ minutes on slow runners. Check runner logs for stalled steps. |
| `Artifact download fails` | GitHub Actions sometimes has issues downloading artifacts. Retry the workflow. |

### Version Issues

| Symptom | Cause | Fix |
|---|---|---|
| `make bump-patch` produces wrong version | Check `src/project.godot` format; `config/version="0.9.1"` is canonical | — |
| Release workflow doesn't trigger | See CI Workflow Suppression above | Re-run manually |
| GitHub Release already exists for this version | Tag conflict | Bump to next patch |
| Version bump creates no commit | Git config issue | `git config --global user.name "Your Name"` |
| Version mismatch in DMG | `export_presets.cfg` out of sync | `make build` re-syncs automatically |
| Multiple version bumps in git log | Accidental bumps | Squash commits before pushing |

## Architecture & Design

### Why This Process?

1. **Single version source** (`src/project.godot`) prevents sync errors
2. **Automated CI** ensures consistent builds across runs
3. **Notarization** lets players download without Gatekeeper warnings
4. **Universal binaries** support both Apple Silicon and Intel Macs
5. **DMG installers** provide familiar macOS UX
6. **Developer ID + notarization** is required for users to run the app without right-click-open workarounds. Apple's Gatekeeper blocks unsigned/non-notarized apps by default.
7. **Ad-hoc signing** is acceptable for local testing and the "download and run" workflow with right-click-open, but fails for distribution.
8. **Version-driven CI** avoids manual tagging drift — `config/version=` is single source of truth.

### Key Files

| File | Purpose |
|---|---|
| `src/project.godot` | Single source of truth for game version |
| `src/export_presets.cfg` | Godot export configuration (Universal Binary preset) |
| `resources/entitlements.plist` | Hardened runtime entitlements for notarization |
| `scripts/release/bump_version.sh` | Version bumping script |
| `scripts/ci/make_universal.sh` | Merges x86_64 + arm64 into universal binary |
| `scripts/ci/sign_dmg.sh` | Code signing, notarization, DMG creation |
| `scripts/ci/create_dmg.sh` | Legacy simple DMG creation |
| `.github/workflows/release.yml` | GitHub Actions release automation |
| `Makefile` | Local build targets (`build`, `dmg`, `all`) |

### SemVer Convention

Old Growth follows [Semantic Versioning](https://semver.org/):

| Bump | When | Example |
|---|---|---|
| **Patch** `X.Y.Z+1` | Bug fix, hot fix | `0.6.0 → 0.6.1` |
| **Minor** `X.Y+1.0` | Feature complete, phase shipped | `0.6.0 → 0.7.0` |
| **Major** `X+1.0.0` | Epic complete (e.g., 8-phase) | `0.7.0 → 1.0.0` |

## Useful Commands

### List Developer ID Certificates

```bash
security find-identity -v -p codesigning
```

### Check Certificate Validity

```bash
security find-certificate -a -c "Developer ID" ~/Library/Keychains/login.keychain-db
```

### View Notarization History

```bash
xcrun notarytool history --apple-id "your-apple-id@example.com" --team-id "A1B2C3D4E5"
```

### Check Notarization Status

```bash
xcrun notarytool log <submission-id> --apple-id "your-apple-id@example.com" --team-id "A1B2C3D4E5" --password "app-specific-password"
```

### Validate DMG Signature

```bash
codesign -vvv "build/OldGrowth-macOS-0.7.0.dmg"
```

### Verify Gatekeeper Approval

```bash
spctl --assess --type open --context context:primary-signature "build/OldGrowth-macOS-0.7.0.dmg"
# Should output: "accepted"
```

### Verify Notarization Staple

```bash
xcrun stapler validate "build/OldGrowth-macOS-0.7.0.dmg"
```

## Frequently Asked Questions

**Q: Can I release to Windows/Linux yet?**
A: Not yet. macOS `.dmg` is implemented. Windows `.msi` and Linux `.AppImage` are planned.

**Q: Do I need an Apple Developer Program membership?**
A: Only if you want to sign with Developer ID and notarize. Ad-hoc signing (for local testing) doesn't require it.

**Q: What if notarization fails?**
A: The `sign_dmg.sh` script logs Apple's rejection reason. Common issues: code signature invalid, hardened runtime incompatibility, entitlements problems. Fix the issue and retry.

**Q: Can I test notarization locally without Apple ID?**
A: No, but you can use ad-hoc signing for full local testing: `make dmg`. The DMG won't be Gatekeeper-approved, but the build process is identical.

**Q: How do I roll back a version bump?**
A: `git reset --soft HEAD~1` to undo the commit, then `git checkout src/project.godot` to restore the old version.

**Q: Can I build multiple architectures on a single Mac?**
A: Yes. `make rust-universal` uses cross-compilation targets (`aarch64-apple-darwin`, `x86_64-apple-darwin`) to build both from one machine.

**Q: Why does the app take so long to export?**
A: Godot is reimporting assets and compiling scripts. Subsequent exports are faster. Use `make godot-cache-clean` only if export fails.

**Q: Can I use a self-signed certificate?**
A: Yes for ad-hoc signing (the default). For Developer ID, you must have a real certificate from Apple.

**Q: Can I use an organization (team) Developer ID instead of personal?**
A: Yes. The process is the same, but you'll need a team-signed certificate. Apple requires additional legal entity verification for organizations.

**Q: What if my Developer ID certificate expires?**
A: Developer ID certificates last 3 years. You'll need to request a new one from developer.apple.com before it expires. Update the `.p12` export and GitHub secrets.

**Q: Can I sign multiple apps with the same certificate?**
A: Yes. The certificate is tied to your Apple ID, not a specific app. You can use it to sign any app you distribute under your name/organization.

**Q: Is the app-specific password stored anywhere on my machine?**
A: No. The password is only transmitted to Apple's notarization API. You need to save it yourself (or regenerate it from appleid.apple.com). GitHub Actions stores it as a secret.

**Q: What if I lose my `.p12` file or password?**
A: You can re-export from Keychain as long as the certificate is still there. If the certificate is gone, request a new Developer ID certificate from developer.apple.com.

**Q: Do I need different certificates for different platforms (Intel vs Apple Silicon)?**
A: No. The same Developer ID certificate works for all architectures (x86_64, arm64). The signature is applied to the universal binary.

**Q: Can GitHub Actions sign and notarize automatically without me doing anything?**
A: Yes. Once you've set up the GitHub secrets, the `.github/workflows/release.yml` workflow runs automatically when you push a version bump to main. No manual intervention needed.

## Related

- [[Build System]] — the local Make-driven workflow and pre-commit hooks
- [[Component SemVer Policy]] — component versioning (separate from game version)
- [[CI Pipeline]] — the automated CI checks and when they run
- [[Project Topology]] — autoloads, module layout, file organization
- [[Tech Stack Review]] — macOS packaging decision (codesigning, DMG, platform scope)
- [[Code]] — folder MOC
- [[Constitution]] — Article III Rule 15 (game version bump obligation)
ember's answer for note_placement-code-release-and-distribution, placementProbability ember gave each option.Art0.01Code0.07 goldDecisions0.06Design0.06Discoveries0.02Governance0.05Narrative0.01Reference0.11Sessions0.01Systems0.60 ember's pick

note_placement-discoveries-mode-gated-key-sharing-pattern: placement

Note placement · test
Gold
Place the note under Discoveries/ (Discoveries)
ember
Place the note under Code/ (Code)
Signal
confidence 0.44 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Discoveries/Mode-Gated Key Sharing Pattern.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Mode-Gated Key Sharing Pattern

The same `restart_scene` input action (bound to the R key) serves two different functions depending on game mode, without conflicting:

- **ON_FOOT mode**: `Input.is_action_just_pressed("restart_scene")` triggers the bus respawn (`_request_bus_respawn`). This fires once on key-down.
- **DRIVING mode**: `Input.is_action_pressed("restart_scene")` triggers the stuck recovery hold-timer. This is checked each frame while held, and requires a 1-second hold duration.

## How They Don't Conflict

The two handlers use different detection methods on the same action:

| Mode | Detection method | Trigger condition |
|------|-----------------|-------------------|
| ON_FOOT | `is_action_just_pressed` | Single frame on key press |
| DRIVING | `is_action_pressed` with hold timer | Held for 1 second |

`is_action_just_pressed` fires once on the exact frame the key transitions from released to pressed. `is_action_pressed` returns true for every frame the key is held. They operate independently — the just-pressed handler fires and completes before the hold timer reaches 1 second, so there is no race condition.

Additionally, the on-foot handler is mode-gated in `_on_bus_respawn_requested` which only processes the request when `GameState.player_mode == ON_FOOT`. The recovery handler only runs in DRIVING mode, gated by `_is_stuck`.

## Application

This pattern can be reused whenever the same physical key needs different behavior across game modes. Key requirements:
1. One mode uses `just_pressed` (instantaneous), the other uses `pressed` (continuous with hold timer)
2. The instantaneous handler must complete in well under the hold timer duration
3. Both handlers are mode-gated so they never process the other mode's event

## References

- src/scripts/bus/samba_bus.gd — `_check_recovery_input` uses `is_action_pressed`
- src/scripts/player/on_foot_controller.gd — `_request_bus_respawn` uses `is_action_just_pressed`
ember's answer for note_placement-discoveries-mode-gated-key-sharing-pattern, placementProbability ember gave each option.Art0.00Code0.44 ember's pickDecisions0.05Design0.10Discoveries0.08 goldGovernance0.01Narrative0.00Reference0.08Sessions0.01Systems0.22

note_placement-governance-tech-stack-review: placement

Note placement · test
Gold
Place the note under Governance/ (Governance)
ember
Place the note under Decisions/ (Decisions)
Signal
confidence 0.82 (verify band)
Kit outcome
acted, flagged for verification
Why this gold label
Mined from Governance/Tech Stack Review.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Tech Stack Review

The technology stack decisions for Old Growth — what was chosen, what was evaluated and rejected, and where the open risks are. The authoritative approved stack lives in constitution Appendix A; this note captures the *why* behind each decision.

## Overview

Old Growth's stack is deliberately conservative where possible and opinionated where necessary. The engine and scripting language are locked by constitution (Article III). Vehicle physics, terrain, procedural generation, and audio are locked with strong alternative-assessment. A handful of visual effects and content questions remain open.

**Status legend:**
- 🟢 **Locked** — researched, tested compatibility, no meaningful alternative
- 🟡 **Strong lean** — best option found but alternatives exist worth discussing
- 🟠 **Open** — decision made but could reasonably go another way
- 🔴 **Unresolved** — no decision yet or significant uncertainty

## Key Decisions — Core Stack (all 🟢)

| System | Pick | Why |
|--------|------|-----|
| Game engine | Godot 4.6+ | Native Metal renderer, plaintext GDScript (agent-readable), MIT, Jolt built-in, strong macOS export |
| Scripting | GDScript + Rust (gdext) | GDScript for scene glue; Rust for performance-critical (Article III Rule 17) |
| Physics | Jolt (native in 4.6+) | More stable vehicle sim than Godot Physics; GEVP-compatible |
| Target platform | macOS Universal Binary | Scope constraint; Apple Silicon (Metal) + Intel Macs |
| Physics tick | 120 Hz | GEVP requires 120+; 60 causes instability; 240 is unnecessary |

## Vehicle Physics

- **Vehicle controller**: GEVP (vendored in `addons/`, MIT). Raycast-based, keyboard-friendly, single-script, simcade preset. Needs tuning for 1963 VW Samba feel (rear-engine RWD, ~1100 kg, ~50 HP, high CoG, soft suspension, long braking distances).
- **Engine audio**: Pitch-scaled `AudioStreamPlayer3D` loops (🟠). Simplest approach; may upgrade to multi-sample crossfade in audio polish.

## Terrain & Roads

- **Terrain system**: Terrain3D (C++ GDExtension, MIT). GPU clipmap, foliage instancer, 32 textures, Road Generator connector.
- **Road generation**: Godot Road Generator (🟡). Designed for hand-placed roads; procedural usage is supported but less documented. **Technical risk for spec 053 remediation.**
- **Foliage**: Terrain3D built-in instancer (🟡). Must handle biome-specific vegetation swaps.
- **Heightmap**: FastNoiseLite (Godot built-in). Sufficient for ~2km² worlds.

## Weather & Atmosphere

- Rain-on-Glass shader (🟡), custom wiper integration (🟠)
- Volumetric fog: Godot 4 built-in (🟢)
- Sky: ProceduralSkyMaterial (🟡) — must produce three distinct biome sky feels
- Weather zones: Voronoi/grid subdivision (🟡, novel system)

## Visual Effects & Post-Processing

- Post-processing: ArseniyMirniy Color Correction (🟢, vendored)
- Windshield crack overlay (🟠), Storm wall shader (🟠), biome transition blending (🟡)

## Anomaly Systems

Most anomalies use standard Godot primitives (AnimationPlayer, Area3D, GPUParticles3D, OmniLight3D, MeshInstance3D). See [[Anomalies]] for design specifications; tech choices here are typically 🟢 Locked.

## Audio

- **Audio management**: Godot native (🟢). Sound Manager (nathanhoad) evaluated and rejected (insufficient — no AudioStreamPlayer3D, no AudioEffect manipulation, ambient pool limited to 1 stream).
- **Bus architecture**: 7 buses planned per constitution Appendix A (Master → Ambience, Proximity, Interior, Horror, Radio, RadioDegraded). Currently only Master + Radio + RadioDegraded exist in `src/default_bus_layout.tres`; the remaining buses are being built incrementally through the audio specs. See [[Audio — Overview]] for the CONFLICT callout and [[Conflicts]] for the indexed entry.
- **Positional**: `AudioStreamPlayer3D` built-in (🟢)

## On-Foot Systems

- CharacterBody3D + custom FPS controller (🟡)
- Car entry/exit: custom camera reparent + input context switch (🟠) — **highest-risk custom system**, ~200 lines
- Combat: melee raycast + ranged hitscan/projectile (🟢) — per Article II, on-foot scope is strictly bounded
- Enemy AI: StateMachine + NavigationAgent3D (🟡) — **must verify NavMesh bakes on Terrain3D at runtime**

## Disturbance & Storm

- Disturbance meter: autoload with add(amount, source) + threshold signals (🟢, matches Article X)
- Disturbance HUD: organic visual indicator (🟠, creative risk)
- Storm wall direction: spawn opposite nearest exit, advance toward player (🟢)

## Persistence & Architecture

- **Save**: Godot Resource serialization (`.tres`) (🟡). World state NOT saved — only garage/player state (Article IV Rule 6).
- **Settings**: `ConfigFile` at `user://settings.cfg` (🟢)
- **Cross-system communication**: Signal bus autoload (`events.gd`) (🟢, Article VI)
- **Game state**: `GameState` autoload + enum states (🟡)

## Assets & Pipeline

- **3D format**: glTF 2.0 (.glb) (🟢)
- **VW Samba model**: Placeholder (Kenney) → custom/purchased (🟠). No perfect free 1963 VW Type 2 Samba (23-window, V-nose, two-tone, curved skylight).
- **PBR textures**: CC0 (ambientCG, cgbookcase, Polyhaven) (🟢)
- **Audio assets**: CC0 (Freesound, OpenGameArt, Kenney) (🟡) — fungal/stone SFX may need original recording
- **Radio music**: 🔴 UNRESOLVED. Need 30+ min across multiple genres. CC0 music is rare; AI-generated is an option; doesn't block until audio polish phase.

## macOS Packaging

- Code signing: Apple Developer ID ($99/yr) or ad-hoc (🟠)
- Distribution: DMG with drag-to-Applications (🟢)
- Game controller: Godot GameController built-in (🟢)

## Highest-Risk Open Questions

The source doc identifies six items as the highest technical risks:

1. **Procedural road algorithm** — connected road network from a seed over ~2km² (spec 053)
2. **NavMesh on Terrain3D** — does NavigationAgent3D bake reliably? If not, fall back to raycast steering.
3. **Biome texture swap in Terrain3D** — texture-set-per-region capability
4. **World generation performance** — ~2km² world in <5 seconds on M4 Max (Article V)
5. **Petrified Zone screen-space shader** — selective desaturation of meshes in a 3D area without affecting HUD
6. **Storm wall directional shader** — dramatic shader for an advancing flat plane

## Summary Counts (per source)

- **🟢 Locked**: 26 decisions
- **🟡 Strong lean**: 15 decisions
- **🟠 Open**: 8 decisions
- **🔴 Unresolved**: 1 decision (radio music content)

## Related

- [[Constitution]] — Article III and Appendix A are the authoritative source
- [[Component SemVer Policy]] — how these components are versioned
- [[Governance]] — folder MOC
- [[Build System]] — the CI pipeline that enforces these choices

## Agent Notes (backlinks)

- [[Decisions/Rust-GDScript Split Architecture]] — Rust/GDScript split decision with operational constraints
- [[Audio Generation Pipeline Constraints & Patterns]] — TTS engine selection, Python 3.11 requirement, provenance schema, ElevenLabs SFX V2 pipeline
- [[Vault Tooling — Constraints and Patterns]] — vault audit, Observer QA, asset export safety
- Vault Tooling — Constraints and Patterns — vault audit, asset export, Observer QA, development constraints
ember's answer for note_placement-governance-tech-stack-review, placementProbability ember gave each option.Art0.01Code0.02Decisions0.82 ember's pickDesign0.04Discoveries0.01Governance0.04 goldNarrative0.01Reference0.02Sessions0.01Systems0.02

note_placement-art-visual-direction: placement

Note placement · dev
Gold
Place the note under Art/ (Art)
ember
Place the note under Systems/ (Systems)
Signal
confidence 0.33 (defer band)
Kit outcome
deferred: gather evidence or ask
Why this gold label
Mined from Art/Visual Direction.md at 692f4bb58b7f62105aeb73fba8dfee12bfdecd53; gold is its actual folder.
Visual Direction

The overall visual direction for Old Growth AND the complete agent-driven visual asset generation pipeline — Flux.1 [schnell] + ComfyUI + custom LoRA + "artisan" agent producing textures and concept art. This note captures both the aesthetic framing and the authoritative technical pipeline specification.

## Overview — Aesthetic Direction

Old Growth pursues **stylized realism**. Atmospheric mood takes precedence over graphical fidelity (Article IX). Key references: Simon Stålenhag's paintings, the organic corruption of *Annihilation*, the zone hazards of *Roadside Picnic*, the deep woods of *Twin Peaks*, and the ruined overgrowth of *The Last of Us*.

**Atmosphere is a design constraint, not decoration** (Article IX, Article XII-K). Every new system, asset, and mechanic is evaluated against: *Does this serve or undermine the feeling that this forest is ancient, alive, and hostile?* If it undermines it, it is redesigned or removed.

The visual direction serves the central horror: **the player is very small inside a condition that does not notice them**. Scale must feel overwhelming at all times. An 80-meter tree makes the Samba look like a toy. The player should never feel large in this world.

## Design Intent — Key Decisions

- **Stylized realism, not photorealism**: composition, color, value, light shape are the primary tools. Polygon counts and shader complexity are secondary. Painted, not rendered. Silhouette first — any new asset must read silhouette-first at gameplay distance.
- **Biome as palette gradient** (see [[Palettes]]): color grading shifts per biome. The gradient itself is the storytelling device. Transitions between biomes are gradual — light, sound, and bus behavior shift before the player consciously names the change.
- **Samba silhouette as identity anchor** (Article XII-G): the Samba's silhouette is a primary identity mark. Readable at any time of day or weather. New weather states or lighting conditions must be validated against Samba silhouette readability before shipping.
- **Motivated light**: all scene lighting must appear to come from plausible in-world sources (sun angle, headlights, bioluminescence, fire). Unmotivated fill lights prohibited unless behind a debug-only flag.
- **Night is dark**: not atmospherically dark — *genuinely* dark, the kind that requires headlights for basic navigation. Lets Amber Bleed anomaly's amber glow read clearly from a distance, and makes the garage window's view matter.
- **Visual language is consistent**: players learn anomaly signatures from a distance. Mycelium has a specific luminosity. Hollow Trees have a specific silhouette. Petrified Zones' grey particulate accumulates in recognizable patterns. The world teaches its own grammar (Article XII-G).
- **Value contrast passes greyscale**: all critical visual communication must be tested in greyscale before shipping (Article XII-G). A composition that only works in color is fragile.
- **Accessibility**: color-coded information must be paired with a secondary differentiator (shape, value, icon). Deuteranopia/protanopia simulation must be run before finalizing any color-coded system.

## Visual Asset Pipeline — Overview

The pipeline is an agent-driven visual asset generation workflow producing texture/material candidates and concept art/reference imagery. It mirrors the audio pipeline architecture (submodule + Git LFS + spec-driven agent dispatch) and slots into the Concourse infrastructure alongside existing MLX inference services.

**What this is NOT**: a replacement for hand-crafted or purchased assets. The pipeline produces candidates and reference material, subject to human review and acceptance before integration into Godot.

## Stack

| Component | Role | Details |
|---|---|---|
| **Flux.1 [schnell]** | Primary generation model | Apache 2.0 — fully commercial. 4-step generation. fp16 full precision viable at 128 GB. Quality gap vs [dev] is real but closed significantly by a well-tuned LoRA. |
| **Flux.1 [dev]** | ❌ Excluded | Non-commercial license only. Not viable for a shipping title. |
| **ComfyUI** | Workflow engine | Runs natively on Apple Silicon via MPS + MLX extension. Port 8189 (offset from existing inference on 8001/8002). |
| **ComfyUI-MLX** | Acceleration layer | 50–70% speed improvement over PyTorch MPS on M-series. Installed via Custom Nodes Manager. |
| **comfyui-mcp** | MCP server | `@peleke.s/comfyui-mcp` via npx. Exposes 40 tools (txt2img, img2img, upscale, inpaint, outpaint, ControlNet, LoRA injection). Auto-detects Flux (natural language) vs SDXL (tag) prompting strategy. |
| **artisan agent** | oh-my-openagent agent | Reads biome specs, constructs prompts, dispatches generation jobs via comfyui-mcp, organizes outputs. |

## Infrastructure Setup

### ComfyUI Installation

```bash
cd ~/concourse
git clone https://github.com/comfyanonymous/ComfyUI
cd ComfyUI
python -m venv ../.venv-comfyui   # outside repo to avoid git noise
source ../.venv-comfyui/bin/activate
pip install torch torchvision torchaudio  # MPS backend, Apple Silicon
pip install -r requirements.txt

# Launch on non-conflicting port
python main.py --port 8189 --listen 127.0.0.1
```

### ComfyUI as Persistent Service (launchd)

Full plist at `~/Library/LaunchAgents/com.concourse.comfyui.plist` runs ComfyUI at login with `RunAtLoad`/`KeepAlive` true, logs to `/tmp/comfyui.{log,err}`. Load with `launchctl load ~/Library/LaunchAgents/com.concourse.comfyui.plist`. Full plist XML in archived spec.

### MLX Extension

ComfyUI Manager → Custom Nodes Manager → search "ComfyUI MLX" → Install. Native MLX inference nodes for Flux models.

### Model Weights Layout

```
ComfyUI/models/
├── checkpoints/
│   ├── flux1-schnell-fp8.safetensors  # inference — Apache 2.0, commercial OK
│   └── flux1-schnell.safetensors      # full precision — for LoRA training only
├── text_encoders/
│   ├── clip_l.safetensors
│   └── t5xxl_fp16.safetensors         # fp16 recommended with 128GB
├── vae/
│   └── ae.safetensors
├── loras/
│   └── pnw-oldgrowth-v1.safetensors  # trained PNW style LoRA
└── upscale_models/
    └── RealESRGAN_x4plus.pth          # 4x upscale for final texture exports
```

### MCP Server Registration

Add to oh-my-openagent MCP config:

```json
{
  "comfyui": {
    "command": "npx",
    "args": ["-y", "@peleke.s/comfyui-mcp"],
    "env": {
      "COMFYUI_URL": "http://127.0.0.1:8189",
      "COMFYUI_MODEL": "flux1-schnell-fp8.safetensors"
    }
  }
}
```

## Artisan Agent

### Role Definition

```yaml
name: artisan
port: null  # stdio MCP, no inference port
model: claude-sonnet-4  # local not needed; task is orchestration not inference
tools:
  - comfyui (txt2img, img2img, upscale, list_models, list_loras)
  - filesystem (read biome specs, write output manifests)

responsibilities:
  - Read canonical biome specs and extract visual descriptors
  - Construct Flux-optimized natural language prompts
  - Dispatch generation jobs with correct workflow type (texture vs concept)
  - Organize outputs into asset submodule structure
  - Write generation manifests for reproducibility

out_of_scope:
  - Asset acceptance/rejection (human review gate)
  - Godot import configuration
  - LoRA training
```

### Task Dispatch Schema

```yaml
task_type: generate_assets
biome: verdant_old_growth         # canonical biome name
asset_type: texture               # texture | concept_art
material_class: bark              # see Asset Taxonomy below
batch_size: 4                     # candidates per run
seed: 42                          # null for random; set for reproducibility
lora:
  name: pnw-oldgrowth-v1
  strength: 0.75
output_dir: assets/textures/verdant_old_growth/bark/
```

## Biome Prompt Schema

### Canonical Biome Descriptors

Each biome encodes the visual language artisan uses to construct Flux prompts.

**Verdant Old Growth**:
```yaml
palette:
  dominant: ["deep forest green", "rich brown", "dark umber"]
  accent: ["pale fern green", "moss yellow-green", "filtered golden light"]
  temperature: cool-neutral
  saturation: high
lighting:
  quality: diffused, overcast Pacific Northwest canopy light
  shadows: soft, deep, layered
  atmosphere: morning mist, light fog between trunks
flora:
  dominant: ["Douglas fir bark", "western red cedar", "sword fern", "oxalis"]
  ground: ["decomposing leaf litter", "duff", "nurse log"]
  micro: ["lichen patches", "liverwort", "club moss"]
mood_keywords: ["ancient", "cathedral quiet", "primordial", "damp"]
negative_prompts: ["sunlit clearing", "dry", "desert", "tropical", "bright sky"]
```

**Fungal Depths**:
```yaml
palette:
  dominant: ["near-black brown", "deep slate grey", "dark purple-brown"]
  accent: ["pale bioluminescent blue-white", "amber mycelium", "rust orange spore"]
  temperature: very cool, desaturated with isolated warm glow
  saturation: low overall, high on fungal accents
lighting:
  quality: near-darkness with self-illuminating fungal sources
  shadows: extremely deep, minimal fill
  atmosphere: still air, spore-laden, no wind
flora:
  dominant: ["oyster mushroom clusters", "turkey tail fungus", "giant puffball"]
  ground: ["dense mycelium network", "decayed wood pulp", "white hyphal threads"]
  micro: ["bioluminescent fungal patches", "spore clouds"]
mood_keywords: ["primordial", "alien", "silent", "decomposition", "network"]
negative_prompts: ["sunlight", "bright", "colorful flowers", "living trees"]
```

**The Petrifold**:
```yaml
palette:
  dominant: ["grey-blue mineral", "bleached white", "pale ash"]
  accent: ["rust iron stain", "pale ochre mineral deposit", "ice blue crack"]
  temperature: cold, desaturated
  saturation: very low
lighting:
  quality: flat grey overcast, harsh shadowless diffusion
  shadows: minimal, diffuse
  atmosphere: cold mineral stillness, no organic warmth
flora:
  dominant: ["petrified tree trunk", "calcified branch", "mineral deposit column"]
  ground: ["cracked mineral surface", "calcified root network", "ash grey soil"]
  micro: ["crystal inclusions", "iron oxide streaks", "pale lichen on stone"]
mood_keywords: ["ancient death", "geological time", "silence", "crystalline"]
negative_prompts: ["green", "living", "warm", "lush", "brown soil"]
```

### Prompt Construction Rules

Artisan uses these templates. **Flux.1 uses natural language — do NOT use comma-separated tag lists.**

**Texture prompt template**:
```
A seamless tileable texture of [material], [biome.flora.dominant context],
[biome.palette.dominant colors], [biome.lighting.quality],
[biome.mood_keywords], highly detailed surface, macro photography,
physically based rendering material, no horizon, no sky, flat lay perspective
```

**Concept art prompt template**:
```
[subject or scene description], [biome name] forest, Pacific Northwest,
[biome.palette.dominant and accent colors], [biome.lighting.quality],
[biome.atmosphere], [biome.mood_keywords], cinematic composition,
environmental concept art, matte painting style
```

**Negative prompt (all types)**:
```
[biome.negative_prompts], watermark, signature, text, blurry,
low quality, cartoon, anime, illustration style, flat design
```

## Asset Taxonomy

### Textures

Target: 1024×1024 seamless tiles, upscaled to 2048×2048 for Godot import.

**Important**: Flux does not natively generate seamless/tileable textures. The ComfyUI workflow for textures must include a seamless tiling node chain. Use the `SeamlessTile` custom node (available via ComfyUI Manager) or circular padding in the VAE decode step. Without this, textures will show visible seams when tiled.

**PBR map pipeline**: Godot's `StandardMaterial3D` uses albedo, normal, roughness, AO maps. This pipeline generates albedo directly. Derive remaining maps from accepted albedo outputs using:
- **Normal map**: `generate-texture` node in ComfyUI with a normal map ControlNet, or post-process in Materialize / AwesomeBump (free, offline)
- **Roughness/AO**: Materialize or xNormal from accepted albedo — automated batch processing feasible
- Normal/roughness derivation is out of scope for artisan agent — post-acceptance step before Godot import

```
textures/
├── verdant_old_growth/
│   ├── bark/           # douglas_fir, cedar, alder, mossy_bark
│   ├── ground/         # forest_duff, leaf_litter, exposed_root, muddy_soil
│   ├── rock/           # mossy_boulder, exposed_basalt, lichen_rock
│   └── organic/        # fern_frond, sword_fern_bed, oxalis_patch, club_moss
├── fungal_depths/
│   ├── fungal/         # oyster_cluster, turkey_tail, puffball, mycelium_sheet
│   ├── ground/         # decayed_wood_pulp, dense_mycelium, hyphal_mat
│   └── wood/           # heavily_decayed, nurse_log_surface, heartwood_rot
└── the_petrifold/
    ├── mineral/        # petrified_bark, calcified_surface, mineral_column
    ├── ground/         # cracked_mineral, ash_soil, calcified_root
    └── crystal/        # iron_stain, crystal_inclusion, calcite_vein
```

Each accepted albedo texture gets a sibling set before Godot import:
```
bark_douglas_fir_0042_001_albedo.png
bark_douglas_fir_0042_001_normal.png
bark_douglas_fir_0042_001_roughness.png
bark_douglas_fir_0042_001_ao.png
```

### Concept Art / Reference

Target: 1920×1080 or 1280×720. Not for direct game use — reference and pitch material only.

```
concept/
├── verdant_old_growth/
│   ├── wide/           # full environment establishing shots
│   ├── road/           # forest road reference for driving sections
│   └── detail/         # close-up flora reference
├── fungal_depths/
│   ├── wide/
│   ├── bioluminescence/  # lighting reference for Godot environment shader
│   └── detail/
└── the_petrifold/
    ├── wide/
    ├── transition/     # boundary zones between biomes
    └── detail/
```

## Output Structure & Git LFS

### Submodule

```bash
git submodule add git@github.com:shapeandshare/old-growth-assets assets
cd assets
git lfs install
```

### LFS Tracking

```bash
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.exr"    # for HDR concept exports
git lfs track "*.safetensors"  # track LoRA weights in assets repo
git add .gitattributes
```

### Asset Filename Convention

```
{material_slug}_{seed}_{index}.png
# e.g. bark_douglas_fir_0042_001.png
#      mycelium_sheet_1337_003.png
#      concept_wide_verdant_0099_001.png
```

- `material_slug` — underscore-separated descriptor matching taxonomy leaf node
- `seed` — zero-padded to 4 digits
- `index` — zero-padded batch index (001–NNN)

### Generation Manifest

Each batch writes a `manifest.json` alongside its outputs:

```json
{
  "generated_at": "2025-04-16T14:30:00Z",
  "model": "flux1-schnell-fp8.safetensors",
  "lora": { "name": "pnw-oldgrowth-v1", "strength": 0.75 },
  "biome": "verdant_old_growth",
  "asset_type": "texture",
  "material_class": "bark/douglas_fir",
  "prompt": "A seamless tileable texture of douglas fir bark...",
  "negative_prompt": "sunlit clearing, dry...",
  "seed": 42,
  "steps": 4,
  "cfg": 1.0,
  "resolution": "1024x1024",
  "outputs": [
    { "filename": "bark_douglas_fir_0042_001.png", "accepted": null },
    { "filename": "bark_douglas_fir_0042_002.png", "accepted": null }
  ]
}
```

`steps: 4` is correct for schnell (distilled 4-step model — more steps do not improve quality). `cfg: 1.0` is correct (schnell is guidance-free). `accepted` is `null` until human review sets to `true` or `false`.

## LoRA Strategy

### Why a LoRA is Essential

Flux.1 [schnell] generates competent but generic forests. The specific Pacific Northwest character — the particular quality of light through a doug fir canopy, the exact color of Oregon sword fern, the texture of nurse log decomposition — requires a fine-tuned style anchor. The LoRA is the highest-leverage single investment in this pipeline.

### Training Data Sources

Training data **must be real Pacific Northwest photography, not AI-generated images**. Real photographs give you:
- Authentic PNW light, color, texture rather than an AI approximation
- Legally clean training data with no ToS ambiguity
- Better LoRA quality — the model learns real-world material properties

**Do NOT use Midjourney outputs as training data.** The legal landscape around using AI-generated images to train other AI models is actively litigated and the ToS is ambiguous. Real CC photography is cleaner and produces better results.

**License requirements**: training data for a commercially-released game must be under licenses that permit commercial use. That means CC BY, CC BY-SA, Public Domain, or platform-specific licenses that explicitly cover ML training. CC BY-NC explicitly prohibits commercial use — do not use NC-licensed images.

**Primary sources (commercial-safe)**:

| Source | What to pull | License | Notes |
|---|---|---|---|
| **USFS Photo Library** | PNW national forest photography, old growth, flora | Public Domain | Cleanest possible — no restrictions |
| **Wikimedia Commons** | Douglas fir, cedar, Hoh Rainforest, Olympic NP, fungi, PNW flora | CC BY, CC BY-SA, PD | Filter by license — skip CC BY-NC and CC BY-ND |
| **Flickr CC** | `old growth forest pacific northwest`, `olympic peninsula`, `sword fern`, `douglas fir bark` | CC BY / CC BY-SA only | Must filter explicitly — default search includes NC. Use `license=4,5` URL params to filter to BY and BY-SA only |
| **Unsplash** | Forest, PNW wilderness, macro flora, fungi, bark textures | Unsplash License | Explicitly permits ML training including commercial products |
| **Pexels** | Same subject as Unsplash | Pexels License | Same — permissive, covers commercial ML training |
| **iNaturalist** | ⚠️ Flora macro for PNW species | CC BY-NC (most) | **Avoid for commercial training.** Most observations default to CC BY-NC. Filter to CC BY only but coverage is thin. Use as visual reference, not training data. |

### Dataset Composition

Target: 200–400 images, balanced across biomes.

```
training_data/
├── verdant_old_growth/    ~150 images
│   ├── wide_canopy/       # full environment, dappled light, fog
│   ├── bark_closeup/      # doug fir, cedar, alder — macro texture
│   ├── ground_flora/      # sword fern, oxalis, moss — ground level
│   └── root_nurse_log/    # decomposition, fallen trees
├── fungal_depths/         ~100 images
│   ├── fungi_macro/       # oyster, turkey tail, puffball — closeup
│   ├── mycelium/          # white hyphal networks on wood
│   └── dark_forest/       # heavily shaded, low light conditions
└── the_petrifold/         ~80 images
    ├── petrified_wood/    # mineral-replaced wood
    ├── mineral_texture/   # calcite, basalt, grey rock closeup
    └── dead_forest/       # standing dead trees, grey/white tones
```

### Training Configuration

**Tool**: AI Toolkit (Kohya-based), compatible with `comfyui-mcp` LoRA upload API.

**Important**: Train on the full precision schnell model (`flux1-schnell.safetensors`), not the fp8 quantized checkpoint. Training on fp8 weights degrades gradient quality. With 128 GB unified memory this is not a constraint — the full ~24 GB model loads comfortably.

```yaml
model: flux1-schnell.safetensors   # full precision for training
training_type: lora
rank: 16
alpha: 16
learning_rate: 1e-4
steps: 2000
batch_size: 1   # Apple Silicon MPS constraint
save_every: 500
```

The trained `pnw-oldgrowth-v1.safetensors` is used with the fp8 inference checkpoint at generation time — LoRA weights are precision-independent.

**Trigger words** injected automatically by artisan per biome:
- `verdant_og_style` → Verdant Old Growth
- `fungal_depths_style` → Fungal Depths
- `petrifold_style` → The Petrifold

### Midjourney's Role

MJ remains useful — just not as training data. Use manually through the web UI for:
- Concept exploration before committing to a biome palette direction
- Composition reference for concept art framing
- Prompt language testing — if a scene reads well in MJ, natural language likely translates to Flux prompts

These outputs inform the pipeline but never enter it as training material or generated game assets.

## Godot Import Configuration

### Texture Import Settings

For all albedo textures imported into `res://assets/textures/`:

```ini
# .import file settings (set in Godot Import dock)
compress/mode = 2          # VRAM compressed (BPTC/S3TC)
compress/high_quality = true
flags/repeat = true        # REQUIRED for seamless tiling
flags/filter = true
flags/mipmaps = true       # REQUIRED — prevents shimmer at distance
process/fix_alpha_border = false
process/premult_alpha = false
```

Normal maps require different settings:
```ini
compress/normal_map = true  # tells Godot to treat as normal map
flags/repeat = true
flags/mipmaps = true
```

### Godot Project Asset Path

```
res://
└── assets/
    └── textures/
        ├── verdant_old_growth/
        │   ├── bark/
        │   │   ├── bark_douglas_fir_0042_001_albedo.png
        │   │   ├── bark_douglas_fir_0042_001_normal.png
        │   │   ├── bark_douglas_fir_0042_001_roughness.png
        │   │   └── bark_douglas_fir_0042_001_ao.png
        │   └── ...
        └── ...
```

The `old-growth-assets` submodule mounts at `assets/` in the Godot project root. Only `accepted/` contents are symlinked or copied into `res://assets/` — candidates never enter the Godot project.

## Human Review Gate

Generated assets land in the assets submodule with `accepted: null` in their manifests. Before any asset is referenced in the Godot project:

1. Review batch in any image viewer
2. Set `accepted: true` or `accepted: false` in `manifest.json`
3. Accepted albedo PNGs move to `assets/accepted/` via helper script
4. Derive normal/roughness/AO maps from accepted albedos (Materialize or similar)
5. All four map types imported into Godot with correct settings (§9.1)

This keeps generation non-destructive and separates candidates from production assets.

## Phase Integration

Pipeline is addendum Phase 27 and can run in parallel with active development phases. Texture generation is gated only on biome specs being stable (Phase 8+). Concept art can begin immediately.

**Dependencies on existing specs**:
- Canonical biome definitions (in Biome Prompt Schema above)
- World audio spec (biome boundaries and atmosphere inform concept art composition)
- Observer harness (can be extended to run visual QA on generated assets)

## Image Acquisition Pipeline

### Prerequisites

```bash
pip install gallery-dl          # Flickr CC batch download
pip install requests Pillow     # image validation script
brew install wget               # USFS / Wikimedia direct downloads
```

### USFS Photo Library (Public Domain)

Browse https://www.fs.usda.gov/photography and search for:
- `old growth`, `Pacific Northwest`, `Olympic National Forest`, `Mount Baker-Snoqualmie`
- `Douglas fir`, `western red cedar`, `sword fern`, `nurse log`
- `fungi`, `mushroom`, `lichen`

Download manually — no bulk API. Organize into `training_data/` by biome and category per Dataset Composition above.

### Wikimedia Commons (CC BY / CC BY-SA / PD)

Use Wikimedia Commons API to batch-download by category, filtered to CC BY, CC BY-SA, and Public Domain. Full Python script `scripts/wikimedia_fetch.py` with license filtering and minimum-resolution check lives in the archived spec. Example invocation:

```bash
python scripts/wikimedia_fetch.py \
  --category "Pseudotsuga menziesii" \
  --licenses "CC-BY-4.0,CC-BY-SA-4.0,PD" \
  --output training_data/verdant_old_growth/bark_closeup/ \
  --min-resolution 512
```

### Flickr CC

`gallery-dl` handles Flickr CC with license filtering. License IDs: 4 = CC BY, 5 = CC BY-SA.

```bash
gallery-dl \
  "https://www.flickr.com/search/?text=old+growth+forest+pacific+northwest&license=4,5" \
  --directory training_data/verdant_old_growth/wide_canopy/ \
  --filter "width >= 512 and height >= 512"
```

Repeat for each biome × subject combination. See archived spec for the full set of canonical Flickr queries.

### Unsplash / Pexels

Both have no usable bulk API for specific searches without an API key. Full `scripts/unsplash_fetch.py` Python script in archived spec — invoke per biome × subject combination from Dataset Composition.

### Dataset Validation

Before captioning, validate the full dataset with `scripts/validate_dataset.py --input training_data/` (full Python source in archived spec). Checks each image is ≥512px minimum edge, mode is RGB or RGBA, and file is not corrupt.

### Caption Generation

AI Toolkit requires a `.txt` caption file alongside each training image. Captions must include the trigger word for the target biome. Full `scripts/caption_dataset.py` (BLIP-2-based auto-captioning) in archived spec. Trigger words:
- `verdant_og_style`
- `fungal_depths_style`
- `petrifold_style`

**Review captions manually after generation.** Auto-captions will be imperfect — correct any that describe the wrong subject or miss the key material.

## Model Download

### HuggingFace Authentication

Flux.1 [schnell] is a gated model requiring HuggingFace account acceptance of the license.

```bash
pip install huggingface_hub
huggingface-cli login   # paste your HF token from hf.co/settings/tokens
# Accept license at: https://huggingface.co/black-forest-labs/FLUX.1-schnell
```

### Download Models

```bash
# Inference model
huggingface-cli download black-forest-labs/FLUX.1-schnell \
  flux1-schnell.safetensors \
  --local-dir ~/concourse/ComfyUI/models/checkpoints/

# fp8 quantized for fast inference (optional, saves ~12GB vs full)
huggingface-cli download Comfy-Org/flux1-schnell \
  flux1-schnell-fp8.safetensors \
  --local-dir ~/concourse/ComfyUI/models/checkpoints/

# Text encoders (shared between schnell variants)
huggingface-cli download comfyanonymous/flux_text_encoders \
  clip_l.safetensors t5xxl_fp16.safetensors \
  --local-dir ~/concourse/ComfyUI/models/text_encoders/

# VAE
huggingface-cli download black-forest-labs/FLUX.1-schnell \
  ae.safetensors \
  --local-dir ~/concourse/ComfyUI/models/vae/

# Upscale model
huggingface-cli download ai-forever/Real-ESRGAN \
  RealESRGAN_x4plus.pth \
  --local-dir ~/concourse/ComfyUI/models/upscale_models/
```

### Verify Downloads

```bash
ls -lh ~/concourse/ComfyUI/models/checkpoints/
ls -lh ~/concourse/ComfyUI/models/text_encoders/
ls -lh ~/concourse/ComfyUI/models/vae/
ls -lh ~/concourse/ComfyUI/models/upscale_models/

# Verify ComfyUI sees all models
curl -s http://127.0.0.1:8189/object_info | python3 -m json.tool | grep -A5 "CheckpointLoaderSimple"
```

## Fine-Tuning Process

### Fine-Tuning Process

#### AI Toolkit Installation

```bash
cd ~/concourse
git clone https://github.com/ostris/ai-toolkit
cd ai-toolkit
python -m venv ../.venv-aitoolkit
source ../.venv-aitoolkit/bin/activate
pip install torch torchvision torchaudio  # MPS backend
pip install -r requirements.txt
```

#### Training Config (`configs/pnw-oldgrowth-v1.yaml`)

```yaml
job: extension
config:
  name: pnw-oldgrowth-v1
  process:
    - type: sd_trainer
      training_folder: output/pnw-oldgrowth-v1
      device: mps
      trigger_word: null   # trigger words are per-image in captions
      network:
        type: lora
        linear: 16
        linear_alpha: 16
      save:
        dtype: float16
        save_every: 500
        max_step_saves_to_keep: 4
      datasets:
        - folder_path: ../training_data/
          caption_ext: txt
          caption_dropout_rate: 0.05
          shuffle_tokens: false
          cache_latents_to_disk: true
          resolution: [512, 768, 1024]
      train:
        batch_size: 1
        steps: 2000
        gradient_accumulation_steps: 4
        train_unet: true
        train_text_encoder: false
        gradient_checkpointing: true
        noise_scheduler: flowmatch
        optimizer: adamw8bit
        lr: 1e-4
        lr_scheduler: cosine_with_restarts
        lr_warmup_steps: 100
        max_grad_norm: 1.0
        dtype: bf16
      model:
        name_or_path: ~/concourse/ComfyUI/models/checkpoints/flux1-schnell.safetensors
        is_flux: true
        quantize: false
      sample:
        sampler: flowmatch
        sample_every: 500
        width: 1024
        height: 1024
        prompts:
          - "verdant_og_style, seamless tileable texture of douglas fir bark, pacific northwest"
          - "fungal_depths_style, seamless tileable texture of oyster mushroom clusters, pacific northwest"
          - "petrifold_style, seamless tileable texture of petrified mineral wood surface, pacific northwest"
        neg: ""
        seed: 42
        walk_seed: true
        guidance_scale: 1.0
        sample_steps: 4
meta:
  name: pnw-oldgrowth-v1
  version: "1.0"
```

Train on the full precision schnell model (`flux1-schnell.safetensors`), not the fp8 quantized checkpoint. Training on fp8 weights degrades gradient quality.

```bash
cd ~/concourse/ai-toolkit
source ../.venv-aitoolkit/bin/activate
python run.py configs/pnw-oldgrowth-v1.yaml
```

Training produces checkpoint saves every 500 steps in `output/pnw-oldgrowth-v1/`. Sample images generate at each checkpoint — use these to monitor progress.

**Checkpoint evaluation:**

| Checkpoint | What to look for |
|---|---|
| Step 500 | Trigger words beginning to activate; textures roughly on-topic |
| Step 1000 | Biome character emerging; PNW palette starting to appear |
| Step 1500 | Consistent trigger word response; correct materials and lighting |
| Step 2000 | Full convergence; all three trigger words producing distinct biome characters |

Signs of overfit: images look like copies of training photos, loss of diversity, prompt details ignored → stop earlier. Signs of underfit: trigger words produce generic results, no visible biome differentiation → train longer.

```bash
# Export final LoRA to ComfyUI
cp output/pnw-oldgrowth-v1/pnw-oldgrowth-v1_000002000.safetensors \
   ~/concourse/ComfyUI/models/loras/pnw-oldgrowth-v1.safetensors
```

### Serving the Model

```bash
# Verify LoRA loads in ComfyUI
curl -s http://127.0.0.1:8189/object_info | python3 -c \
  "import json,sys; d=json.load(sys.stdin); print(d['LoraLoader']['input']['required']['lora_name'][0])"
# Should include: pnw-oldgrowth-v1.safetensors
```

Smoke test via `http://127.0.0.1:8189` — load Flux schnell workflow, add LoRA loader, run with prompt `verdant_og_style, seamless tileable texture of douglas fir bark, pacific northwest`, LoRA strength `0.75`, steps `4`, CFG `1.0`. Expected: PNW bark character, cool-neutral palette, visible moss/lichen detail. If output is generic, increase LoRA strength to `0.85–0.95`.

### Generation Workflow

The texture workflow: **generate → make seamless → upscale**.

**Stage 1: Generate 1024×1024 albedo** (via comfyui-mcp `txt2img`):
```json
{
  "prompt": "verdant_og_style, a seamless tileable texture of douglas fir bark, deep forest green and rich brown tones, diffused Pacific Northwest canopy light, ancient primordial damp, highly detailed surface, macro photography, physically based rendering material, no horizon, no sky, flat lay perspective",
  "negative_prompt": "sunlit clearing, dry, desert, tropical, bright sky, watermark, signature, text, blurry, low quality, cartoon",
  "width": 1024, "height": 1024, "steps": 4, "cfg": 1.0, "seed": 42,
  "model": "flux1-schnell-fp8.safetensors",
  "lora": {"name": "pnw-oldgrowth-v1.safetensors", "strength": 0.75},
  "batch_size": 4
}
```

**Stage 2: Make seamless** — apply via ComfyUI `SeamlessTile` node. The offset-and-blend approach shifts the image by half its dimensions, inpaints the visible seam line at low denoise (0.2–0.35), then shifts back. Two passes (horizontal + vertical) required.

**Stage 3: Upscale to 2048×2048** — use RealESRGAN_x4plus at 2× scale from 1024 (not 4× from 512, which loses generation detail).

## Related

- [[Palettes]] — per-biome color regimes
- [[Biomes]] — the three-register gradient art serves
- [[Biome Behavior]] — biome identity across systems
- [[Tone and Horror]] — tonal discipline the art must carry
- [[Pacific Drive Reference]] — peer reference for PNW + domestic-vehicle horror
- [[Rock Scatter Design]] — biome tinting applied to rocks
- [[Art]] — folder MOC
- [[Tooling — Domain MOC]] — domain MOC
- [[Constitution]] — Articles IX (Atmosphere) and XII-G (Visual Design) are the authoritative sources
ember's answer for note_placement-art-visual-direction, placementProbability ember gave each option.Art0.15 goldCode0.10Decisions0.11Design0.19Discoveries0.02Governance0.02Narrative0.00Reference0.06Sessions0.01Systems0.33 ember's pick