# Scale-Ring Navigation (Simulator) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans or
> superpowers:subagent-driven-development to implement task-by-task. Steps use
> checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add scale-ring navigation to the simulator — an endless rotary-encoder
control (relative, vs the absolute 0–4 experience knobs) that walks a *closed
ring* of neutral "scales of nature" clips, with placeholder AI zoom/warp
transitions between adjacent scales. Add 1–2 cheap, true-PD neutral base clips
(NASA/Hubble cosmos + NOAA Ocean Exploration deep sea) so the ring is
demonstrable. **Defer** the expensive real multi-strength flow-stabilized Right
re-bake until the ring is liked; keep deferring the Pi renderer + serial/firmware.

**Spec:** `docs/superpowers/specs/2026-06-07-scales-library-and-right-axis-pipeline-design.md` §3 (scale navigation & zoom transitions), §2 (scales library), §2.1 (strict-PD sourcing map).

**Architecture:** Python stays the single source of truth. The ring *navigation
math* — step, wrap, which transition clip to play and in which direction — lives
in a new canonical pure module `player/ring.py` (mirrors `player/content.py` /
`player/alteration.py`: frozen dataclasses, pure functions). The simulator's data
layer (`simulator/clips.py`) parses a `ring` section of the manifest into the
ring types; the API exposes the ring and a stateless `advance` move; the browser
holds the current ring index and only *renders* (plays the returned transition
file, then the target scale's clip, with the existing per-knob alteration on top).

**Ordering convention (canonical):** ring index 0 = the *largest* scale (cosmos);
increasing index zooms **inward** toward the smallest. `+1` detent = zoom in,
`-1` = zoom out — both wrap (past the smallest wraps to the largest, the
infinite-zoom payoff, §3). Edge `i` connects `scales[i] → scales[(i+1) % N]`; the
last edge is the small→large wrap seam. One transition clip per edge plays
**forward** when zooming inward and **reversed** when zooming outward, so N scales
need only N transition clips (§3: "one clip per ring edge").

**Deferral, made explicit:** the new cosmos/abyss scale clips carry a raw base
only (no pre-baked Right variants); the `Clip` model already falls back any
unauthored strength to the raw base, so Right on a new scale is a no-op until the
multi-strength flow-stabilized re-bake happens (deliberately deferred). Forest
keeps its existing POC variants.

**Tech Stack:** Python 3.13, FastAPI + pydantic, pytest, frozen dataclasses;
vanilla JS + CSS/SVG for the browser; ffmpeg (system or `imageio-ffmpeg`) for
placeholder media.

---

## File Structure

**Engine (create):**
- `player/ring.py` — `Scale`, `Transition`, `ScaleRing`, `RingMove`,
  `TransitionStep`, `scale_at`, `advance_ring`. Pure logic, no I/O/JSON.

**Simulator (modify):**
- `simulator/clips.py` — add `load_ring(manifest)` building a `ScaleRing` from the
  manifest `ring` section + `ring_to_dict` / `ring_move_to_dict` serializers.
- `simulator/app.py` — `GET /api/ring`, `POST /api/ring/advance`.
- `simulator/static/{index.html,app.js,style.css}` — endless-encoder control
  (zoom-in/zoom-out buttons + mouse-wheel on the stage), current-scale readout,
  multi-clip active-clip selection, transition playback.
- `simulator/sample_media/manifest.json` (committed) — add cosmos + abyss clips
  and the `ring` section; media binaries stay gitignored.
- `simulator/setup_sample_media.py` (or sibling) — generate cheap cosmos/abyss
  base placeholders + per-edge transition placeholders (ffmpeg); record true-PD
  provenance in the manifest. Real PD fetch is a best-effort `--fetch` bonus.

**Tests (create/extend):**
- `tests/test_player_ring.py` (create) — ring navigation logic (step, chain, wrap,
  direction/reverse, degenerate N≤1).
- `tests/test_clips.py` (extend) — `load_ring`, serializers.
- `tests/test_simulator_api.py` (extend) — `/api/ring`, `/api/ring/advance`.

**Docs:**
- `docs/ROADMAP.md` — scale-ring status.
- `docs/USER_GUIDE.md` — simulator "scale ring" gesture (if present).

---

## Task 1: Engine — `player/ring.py` (pure ring navigation)
- [ ] TDD `Scale`/`Transition`/`ScaleRing` construction + validation (N edges for N scales; ≥1 scale; degenerate N≤1 ring).
- [ ] TDD `scale_at` (mod indexing) and `advance_ring(ring, from_index, delta)` → `RingMove` (to_index by mod; per-unit `TransitionStep` list; `reversed` on outward; `wrapped` flag on crossing the seam; chained steps for |delta|>1).

## Task 2: Simulator data layer — `load_ring` + serializers
- [ ] TDD `load_ring(manifest)` parsing the `ring` section into a `ScaleRing`.
- [ ] TDD `ring_to_dict` / `ring_move_to_dict`.

## Task 3: API — `/api/ring`, `/api/ring/advance`
- [ ] TDD `GET /api/ring` (scales + transitions, with each scale's title) and `POST /api/ring/advance` (validates from_index/delta; returns the move + target clip_id).

## Task 4: Media — cosmos + abyss bases + transitions; manifest
- [ ] Add cosmos + abyss clips and the `ring` section to the committed manifest with true-PD provenance.
- [ ] Extend the media-setup script to generate cheap base + transition placeholders (and optional real PD fetch).

## Task 5: Frontend — endless encoder + multi-clip + transition playback
- [ ] Active-clip selection by ring index; zoom-in/out buttons + wheel; current-scale readout; play returned transition file(s), then settle on target clip; keep live alteration on top.

## Task 6: Verify + docs
- [ ] Full `pytest -q` green; boot the sim and exercise the ring by eye/screenshot.
- [ ] Update ROADMAP / USER_GUIDE.
