feat(sim+player): scale-ring navigation (endless encoder) + cosmos/abyss scales

Add scale-ring navigation to the simulator per the scales-library design §3:
an endless rotary-encoder control (relative, vs the absolute 0-4 experience
knobs) walks a closed ring of neutral "scales of nature" clips, with placeholder
AI zoom/warp transitions between adjacent scales and a small->large wrap (the
infinite-zoom payoff).

- player/ring.py (new, canonical pure logic): ScaleRing + advance_ring; one
  transition clip per edge, played forward zooming inward / reversed outward;
  modulo wrap both ways; chained steps for a multi-detent spin. Mirrors
  player/content.py conventions (frozen dataclasses, pure functions).
- simulator: load_ring + ring_to_dict/ring_move_to_dict; GET /api/ring and
  POST /api/ring/advance (Python owns step/wrap/transition math; the browser
  only plays the returned transition(s) then settles on the target scale).
- frontend: endless-encoder control (zoom in/out buttons + stage scroll),
  current-scale readout, multi-clip active-scale selection, transition playback.
- media: two cheap true-PD scales so the ring is demonstrable -- cosmos
  (NASA/Hubble) + abyss (NOAA Ocean Exploration), provenance recorded in the
  manifest, bytes generated as labelled placeholders by setup_scales_media.py.
  The expensive multi-strength flow-stabilized Right re-bake is DEFERRED (new
  scales carry a raw base only) until the ring is liked; Pi renderer +
  serial/firmware remain deferred (simulator-first).
- docs: ROADMAP slice-3 done; USER_GUIDE scale-ring gesture; plan doc.

Verified: pytest 215 passed / 2 skipped (+22 new); sim boots, /api/ring +
/api/ring/advance correct incl. wrap, media served, and a headless-Chrome CDP
drive walked cosmos -> forest -> abyss -> (wrap) -> cosmos with the active clip
reloading correctly.

Spec: docs/superpowers/specs/2026-06-07-scales-library-and-right-axis-pipeline-design.md (§3)
Plan: docs/superpowers/plans/2026-06-07-scale-ring-navigation.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-07 23:40:21 -07:00
parent 6cf1ae08ff
commit 7a50ae41bb
16 changed files with 951 additions and 25 deletions
@@ -0,0 +1,97 @@
# 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 04 experience knobs) that walks a *closed
ring* of neutral "scales of nature" clips, with placeholder AI zoom/warp
transitions between adjacent scales. Add 12 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.