The Music-option tracks are referenced by app.js (MUSIC_PLAYLIST), not the
manifest/ring, so build_static skipped them — Music would 404 on the static
site. Add audio/music/<name>.mp3 explicitly. +1 pytest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an explicit /human-experience-simulator/ → /human-machine-strata/ 301 (the
subpath splat doesn't match the trailing-slash root).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build default app-path → /human-machine-strata; deploy.sh APP_PATH default too.
_write_redirects now emits a permanent redirect /human-experience-simulator/* →
/human-machine-strata/:splat so old links still land. Media (R2 bucket) + Pages
project names are unchanged (not in the benstull.art URL). Updated the build
test (asserts the legacy redirect) + static-build e2e path + README paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per docs/superpowers/specs/2026-06-30-welcome-screen-design.md. Fold the bare
#loading splash, the #motion-warning modal, and the #run-sim button into one
#welcome overlay — the universal entry gate:
- State A (.welcoming): heads-up notice + four output controls (Video ON,
Reduce motion OFF, Audio 2, Language) + 'Launch Simulator' button, with the
'Loading Universe' progress pinned at the bottom (media preloads behind it).
- State B (.loading): pressing Launch while still loading hides the messaging +
button, keeps the controls, and centers the loader; entry happens automatically
once phase-1 finishes. Launch when already loaded enters directly.
- The right .panel is hidden until entry (body:has(#welcome)); welcome control
values carry into the panel inputs on entry. Language + Reduce motion drive the
shared state live (both selects/checkboxes mirror); Video + Audio apply on entry.
- Launch is the single audio-unlock gesture (beginExperience runs in-gesture even
when entry is deferred). Retire WARN_KEY (welcome shows every load); entry uses
the chosen Audio level rather than forcing 2.
- i18n: new welcome.launch key + es/fr/ja for the heads-up (warn.title/body),
reworded to point at the on-screen Reduce-motion control; drop dead run.button.
Tests: new welcome.spec.ts (defaults, live language re-render, State B
deferred-entry, straight-in when loaded, control carry-over, panel hidden);
update altitude-lock/a11y/i18n/loop-recovery/static-build e2e + the Python
audio/credits e2e to the welcome flow; add a window.__hefReady diagnostic seam.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
s0031 removed "About this work" to avoid duplicating it on the separate About
page, but that section WAS the D1 compliance — it declares the altered work is a
derivative offered under CC BY-SA 4.0, which is what satisfies the ShareAlike
obligation for the BY-SA source clips. Without it the live site credited the
BY-SA authors but never declared the derivative share-alike.
Restore the declaration on the credits page, under the heading "License & reuse"
instead of "About this work" so it isn't confused with the separate About page
(which covers the project's intent). Tests assert the declaration is present and
that the heading is NOT "About this work".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CC BY-SA work-license declaration ("About this work") was on the credits
page, but the About page (built on the parallel feat/accessibility-pass) will
own that content; keeping it in both places would duplicate it. Remove the
section from credits.html and make the footage note self-contained (it still
states the footage is altered — the per-clip "indicate modifications" cue stays
with the attributions). Tests updated to assert the declaration is intentionally
no longer on the credits page. The .declaration CSS rule is left in place for the
About page to reuse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CC BY / CC BY-SA footage needs its author + license shown to the viewer, and the
installation alters every frame (a derivative). Per-clip license/source already
shipped in clips.json but only surfaced in the dev panel and internal review
pages — the public experience showed nothing, so the static benstull.art site
was not attribution-compliant.
Option A — add a manifest-driven credits/colophon (credits.html + credits.js):
the page fetches the same clips the experience uses (baked clips.json in static,
the live API otherwise) and renders one attribution block per clip (title,
license, source), HTML-escaped, sorted, never silently dropping a clip. An
"ⓘ Credits & licenses" link in the header (localised en/es/fr/ja) makes it
reachable from the work — what CC's "reasonable to the medium" calls for. Audio
is all PD/CC0, credited as a courtesy block.
Option D1 — the colophon declares the altered work itself is offered under
CC BY-SA 4.0 and states the footage is modified, satisfying the ShareAlike +
"indicate changes" obligations for the BY-SA sources. Non-commercial.
Ships in the static build (added to PUBLIC_ASSETS). Verified: 41/41 clips
credited incl. all 7 CC-BY/BY-SA clips, 0 missing a license. Tests: node units
for creditEntries/creditsListHtml + escaping + page structure/declaration;
pytest build-output assertion; Playwright e2e (page lists attributions, header
links through).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The experience boots silent and waits for a gesture (browser autoplay blocks
an un-gestured audio play()). Until now that gesture was an unlabelled Video
toggle — not obvious to a first-time viewer. Add a "Run simulation" button,
revealed over the black stage once media has preloaded; clicking it turns
Video on, snaps the audio dial to a gentle 2/10, dismisses the prompt, and
starts the soundtrack in that one gesture. Turning Video on directly still
begins the experience too (couples audio in, dismisses the button) via a
shared beginExperience(). Localised en/es/fr/ja.
E2E coverage updated (Playwright + pytest-playwright): button shown after
load, button starts video+audio+dismisses, and direct-Video-on path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run via 'python -m tools.build_static' (avoids tools/http.py shadowing stdlib http).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The post-load autoStart() flipped Video on and lifted audio to 3/10 with no
user gesture. Browser autoplay policy blocks an audio play() made outside a
gesture (muted video survives, sound does not), so auto-start showed video but
stayed silent. Boot SILENT instead and wait for the operator to turn Video on —
that click IS the gesture, and its handler couples audio to 3/10 and starts the
soundtrack in-gesture, so sound reliably comes up with video.
Updates the pytest e2e + Playwright e2e specs to assert boot-silent + the
video-on-couples-audio wiring instead of the old auto-start wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41 clips, 753 string-variants per language, structure-validated (key-sets
and tier-counts identical to en). Escalation semantics preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expanded-pools sky-scale work plus two experience tweaks:
- New `sky` altitude (airplane-window / low-flight layer) between orbit and
coast; RING_ORDER cosmos·orbit·sky·coast·reef·abyss. New clip pools across
all scales, re-baked the full adjacent-pair morph set (all-intra, LFS), and
removed the now-stale cosmos↔abyss transitions the old ring order produced.
- Auto-start: once the preload finishes the experience turns Video on and
raises the audio dial to 3/10 with no user gesture (app.js autoStart()).
- Dropped reef_hawaiian (text overlay — violates the text-free pool rule);
reef pool 7→6, manifest transitions 586→558.
Tests: rewrote tests/test_e2e_audio.py for the dial UI + auto-start; updated
the manifest morph-count assertions and the altitude-scrub target for the new
ring order. 302 pytest + 11 Playwright e2e + 9 node unit all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per plan Task 7. Extracts transition_cmd/reverse_cmd argv builders and adds the
ALL_INTRA flag set (-g 1 -keyint_min 1 -sc_threshold 0) so every baked frame is a
keyframe — smooth arbitrary-frame seeking for the scrub interaction. Unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tasks 3-6. Transition is now a directed clip-pair morph; ScaleRing.morph_for
looks up (from,to)->file. advance_ring returns structural steps (no file);
resolve_move picks each crossed altitude's clip THEN its morph, chained, and
reports the locked target. Fast spin keeps the whole chain (blended) instead
of collapsing. /api/ring/advance takes from_clip_id, draws picks, returns the
resolved chain. Dropped ring_move_to_dict + _rev_file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tasks 1-2 of the altitude clip-pair morph plan. Manifest emits a morph
entry for every video pair in adjacent altitudes, both directions
(<src>__<dst>.mp4 zoom-in + .rev zoom-out); generate_media() bakes them
all via the existing xfade=zoomin recipe keyed by clip member.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of 'no sound', found via the live readout: the operator's /api/ring
returned the ring WITHOUT the per-scale audio field (a server process predating the
audio code) → soundtrackUrl() was null → the element never got a src (readyState 0,
no error). The native test player proved the file itself plays. Fix: client falls
back to a scale-id→soundtrack map (mirrors build_pool_manifest.SCALE_AUDIO) so audio
works without a server restart. Verified: stale-ring sim now plays; audio-first-then-
video leaves both on. +2 E2E regression tests (7 total). Chromium + WebKit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operator follow-ups:
- Remove the tap-to-start wall; show a 'Loading Universe…' splash until all media
preloaded (main() awaits preloadAllMedia + a progress bar), then reveal the app.
- Both Video + Audio toggles default OFF (black + silent at start).
- FIX audio still silent on real Safari: the A/B-crossfade-after-gesture didn't
unlock on Safari (priming srcless elements doesn't unlock them). Use ONE <audio>
element played SYNCHRONOUSLY inside the toggle's click — the reliable Safari
unlock; later altitude swaps reuse the unlocked element.
- First time Video turns on, also turn Audio on (one flip = full experience), set+
played in the same gesture; Audio-on-first stays audio-only.
- Soundtrack follows Altitude via the single element (brief fade swap).
- Verified in headless Chromium AND WebKit: coupling works, soundtrack plays,
video-off blanks, zero JS errors. 290 tests + 5 Playwright E2E pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operator-driven live-UI revision:
- Video + Audio are now Dev-Mode-style on/off toggles (Audio on = soundtrack);
white-noise deferred (removed from the live control; pure machinery kept).
- FIX video-off not blanking: the <video>/<canvas> are GPU-composited and could
paint above the auto-z #black on a real GPU — give #black z-index + its own
layer AND hide the video layers (opacity 0) on video-off.
- FIX no soundtrack on Safari/iOS: Safari unlocks <audio> only when play() runs
INSIDE the user gesture; the start gesture now primes both A/B elements
synchronously, so the async crossfades are allowed to play.
- Toggles wired on 'change' (matches the Dev Mode switch).
- player/audio.py AUDIO_SOURCES -> {off, soundtrack}; tests + E2E updated.
- Verified in headless Chromium AND WebKit: audio plays, video-off blanks both
layers, zero JS errors. 288 tests + 3 Playwright E2E pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E2E asserts in a real headless Chromium: white_noise loads the noise asset,
soundtrack loads the current scale's asset, and Visual=off fades video while
audio keeps playing. Driving it out surfaced + fixed three client issues:
- fadeVolume uses setInterval (rAF stalls when the tab is backgrounded)
- start gesture drops srcless priming (a click grants Chrome autoplay
document-wide; priming empty <audio> only hung the handler / polluted state)
- applyAudio doesn't await play() (awaiting a srcless/buffering play can hang)
pyproject: [e2e] optional dep (pytest-playwright). Full suite 288 passed, 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure arg builders (audio_ops) + thin runner (audio_run) + build_audio_media.py.
Produces the 5 normalized soundtrack loops + a synthesized pink-noise bed under
the gitignored sample_media/audio tree. 6 tests pass (white-noise integration
runs against bundled imageio_ffmpeg).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The affect channel previously gated on BOTH knobs (strength = min(left, right)),
so the analytical Left brain co-controlled the emotion words. Per operator
direction, emotions are now driven by the Right (dreamlike) knob ALONE — Left no
longer gates them. This sharpens the split: Left = measurement (detections +
measurements), Right = the dream AND the feeling it evokes.
- player/alteration.py: `_affect_strength(right) = right` (was min(left,right));
`_affect_intensity` keyed off Right. AffectOverlay + is_identity docs updated
(is_identity still holds: right>0 ⇒ dream.strength>0).
- simulator/static/app.js: renderAffect comments (gating is Right via `strength`);
the client logic already keyed min_level/tier off the passed strength + right.
- Affect design spec: revision note + every min(left,right) → right.
- Tests rewritten: affect is Right-driven (full Right + Left 0 surfaces affect;
Right 0 + Left 4 surfaces none).
271 passed, 2 skipped. Live API verified: left=0/right=4 → strength 4; left=4/
right=0 → strength 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes to the simulator's media handling, both surfaced by the cosmos clip swap.
1. Permanent cache-bust via content hash. New `GET /api/media-versions` returns a
short sha1 token per served file (clip bases + ring transitions + reverses),
cached by (mtime, size) so a re-bake is picked up without a restart. The client
fetches it at boot and appends `?v=<hash>` to every /media URL, so a clip
re-baked under the same path (e.g. a re-sourced cosmos base) gets a fresh URL
the browser can't serve stale — fixing the recurring stale-clip problem at the
root (supersedes the `cache: "reload"` belt-and-suspenders, which stays).
2. Fade the pool-landing swap. The baked ring transition zooms into the scale
PRIMARY, but the rotating pool picks a random member on landing — so a
non-primary pick hard-cut from the primary to the chosen clip (e.g. coast
birdrock -> surfgrass, the artifact the operator saw). On landing, when the pick
differs from the primary, mask the swap behind a short fade through the existing
#black overlay (CSS opacity transition); same-clip landings still do a plain
(re)load. General across all pooled scales.
271 passed, 2 skipped (+2: media-version content-hash + absent-file omission).
app.js syntax-checked; endpoint verified live (29 files; token matches a manual
sha1 of the bytes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `immutable, max-age=1y` media header pinned stale footage in the browser for
a year, so a re-sourced or re-cropped clip (e.g. the de-captioned cosmos) never
showed without a manual cache clear. Switch /media to `no-cache` (store, but
revalidate every load): instant altitude swaps already come from the in-memory
blob preload, so the HTTP cache only matters on (re)load — where a conditional
request is a cheap 304 when unchanged but picks up edited clips immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each altitude change reset the single <video> element's src and re-fetched
the scale's footage over the network, compounded by the dev no-store policy
applying to /media too — so every scale change paid a full fetch+decode.
- simulator/app.py: split the cache middleware — app shell/JS/CSS stay
no-store (by-eye dev iteration), but /media is now cacheable
(public, max-age, immutable) since the clips are static.
- simulator/static/app.js: preload every ring clip (19 base + 5 transition,
~350 MB) into in-memory blob object URLs at startup — non-blocking, the
first scale plays immediately while the rest stream in, ordered current
scale outward. mediaUrl() serves the cached blob when ready, else falls
back to the network. A small progress chip self-removes when done.
- tests: assert app assets stay no-store and /media is cacheable.
269 passed, 2 skipped. Frontend verified by-eye by the operator (no headless
browser in repo yet).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Content-pipeline Increment 2, part 2 (the tooling). Implements design §11.5
(docs/superpowers/specs/2026-06-24-content-pipeline-design.md):
- tools/pipeline/track.py — the stage-5 geometry pass. Classical path: a
hand-seeded normalized box propagated by OpenCV Lucas-Kanade optical flow
(CSRT used instead when a contrib build provides it), sampled to a SPARSE
loop-normalized keyframed track + an appear/disappear window. Optional ML
detect+track path lazy-imports ultralytics (clear ImportError if absent;
base install needs only cv2). Pure helpers (normalize/denormalize, loop_t,
infer_window, sample_track, track_to_annotation) are unit-tested; the cv2
propagation is an opt-in integration test on real abyss_wow footage.
Semantics are never produced here — geometry only.
- Author mode — /author.html + author.js/.css reuse the preview stage: pick a
pool clip, scrub, drag a seed box, Run tracker (or Add as static box), author
the LEFT detail tiers (general -> scientific+fact) + salience, shift-click to
place affect anchors with RIGHT emotion tiers, and Save to manifest. Backend:
POST /api/author/track (runs track_seed on the clip's base) + POST
/api/author/clip (idempotent upsert via tools.pipeline.manifest — keeps media
+ provenance, replaces only authored content, reloads in place). The tracker
propagates box geometry; all strings/scientific names/facts are hand-typed.
- Repointed the pipeline integration test off the retired forest base to the
cosmos pool primary. USER_GUIDE simulator section brought current (pools,
coast, 3-knob Mood, real-time dream, progressive tiers, author mode).
267 passed / 2 skipped (+ track pure + opt-in real-footage + author endpoint
tests). Author UI by-eye review deferred to the operator (no Chrome on this box).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Content-pipeline Increment 2, part 1 (the experience). Implements design §11
(docs/superpowers/specs/2026-06-24-content-pipeline-design.md):
- Rotating pool (§11.1): each ring scale references a POOL of vetted clips
(docs/content-candidate-pool.md); the player picks a random member on landing.
player.ring.Scale gains `pool` + pure `pick_clip_id(scale, r)` (injected
randomness); the pick happens at the API boundary (random.random()), a delta=0
advance is the initial/re-roll pick. clips.py parses/exposes the pool; the
client lands on move.target_clip_id. Back-compat: a scale with only clip_id is
a pool of one.
- Rename ring scale forest -> coast (new land<->sea scale, orbit..reef); ring
order cosmos -> orbit -> coast -> reef -> abyss -> wrap. Cleanup: dropped the
leftover local media (cosmos_traverse, orbit_westcoast, old forest/reef, the
Increment-1 orbit/abyss single bases); new coast-edge transition placeholders.
- Time-windowed tracked labels (§11.2): annotations gain appear/disappear
(loop-normalized, wraps across the seam); a label shows only while on screen
and its box tracks the subject. abyss + reef pools authored as test material.
- Progressive LEFT detail tiers (§11.3): per-object `salience` (first shows at
Left 5-salience) + tiered strings (general -> specific -> scientific -> +fact)
escalating with the Left knob; replaces flat min_level gating. Strings may be a
tier list or a static string (back-compat). No engine change (client reads
overlay.level).
- Progressive RIGHT emotion tiers (§11.4): affect vocabulary escalates basic ->
compound with the Right knob (dream.strength); appearance still gated by
min(left,right). No engine change.
New simulator/build_pool_manifest.py holds the hand-authored content and emits
the 19-clip pool manifest (the reproducible baseline). setup_scales_media.py
marked superseded. 251 passed / 4 skipped (+ ring pool + API pool/window tests).
By-eye visual review deferred to the operator (no Chrome on this box).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- run.py: add resolve_ffmpeg() mirroring setup_scales_media.py; change
process_clip ff param to None and resolve at call time
- test_pipeline_integration.py: gate on resolve_ffmpeg() not PATH ffmpeg;
verify proxy dims with cv2 instead of ffprobe
- ffmpeg_ops.py: add fps= to trim segments in crossfade_loop_args so xfade
receives CFR input (xfade rejects 1/0 rate from raw trim output)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the pre-baked SD restyle variants (too fluid — the painting re-rolled
every keyframe and the flow-warp melted frames) with a deterministic, real-time
dream that holds STILL across the loop; only the footage's own motion moves.
Design: docs/superpowers/specs/2026-06-22-right-axis-deterministic-dream-design.md
Engine (player/alteration.py): Restyle(variant) -> Dream(strength, intensity);
Calibration.right_variant_map -> dream_gain. player/state.py: a Right change is a
LIVE_UPDATE now, not a crossfade (only a clip swap crossfades). Plan dict
restyle -> dream. Tests migrated; 233 green. (No new Python behavior for the
client-render swap below.)
Renderer (simulator front-end): the Right dream is a WebGL2 Kuwahara shader on a
<canvas> over the live video — edge-preserving, so motion/structure stay crisp;
the dream is the same footage, just stylized (no blur). Ramped by the knob; max
goes "trippy" (vivid saturation + ~45deg hue drift + posterize, concentrated near
max via t=intensity^2). Phase-1 luminous-haze (CSS+bloom) was built then retired
by eye (blur read as out-of-focus video). Mood grade rides as a CSS filter on the
canvas; bloom layer removed.
Dev ergonomics that fell out of the iteration:
- /dev/version + a 1s client poll = live-reload (open tab never runs stale
renderer code; reloads on my edits AND server restarts).
- no-cache on the dev server; an on-screen error banner + crash-proof render so a
silent throw can't masquerade as "nothing is changing".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Left-brain HUD look-and-feel pass (judged by eye in the sim):
- corner-bracket targeting reticles (capped arms) replace plain boxes
- translucent label chips, legible over any frame
- two sensor channels: cyan detections (with deterministic confidence
scores) vs amber measurements (center crosshair)
- bbox-sized "ANALYSIS . L{n} . {k} OBJ" status tag, escalating with Left
Affect channel (emotions in the HUD), per
docs/superpowers/specs/2026-06-22-affect-channel-hud-design.md:
- surfaces only when BOTH knobs up; strength = min(left, right)
- stable per-scene palette, more words appear with combined strength
- soft glowing violet words on their own opacity layer (the dream
leaking into the machine's read); math in player/alteration.py
- forest/cosmos/abyss palettes in the manifest; 5 new unit tests
Simulator no-cache middleware so by-eye iteration never serves a stale
app.js / api response (fixes the latent "plan has affect but clip.affect
empty" stale-/api/clips bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right strength curve (level 1-4 -> increasing img2img keyframe strength) is pure
and unit-tested; the restyle engine is a verbatim port of the session-0008 POC
flow_restyle.py (SD img2img keyframes + Farneback optical-flow tweens = calm,
no boiling). torch/diffusers imported lazily so the module + curve test stay light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-detent encoder spin previously chained one full transition per scale
crossed (e.g. +5 ≈ 5 placeholder morphs ≈ 12-15s), which feels sluggish for a
fast spin. Design §3 anticipates this: "transitions chain, or past a speed
threshold a faster blended pass is used."
`player.ring.advance_ring` gains an opt-in `fast_spin_threshold` (canonical
default `DEFAULT_FAST_SPIN_THRESHOLD = 3`). A spin batches its detents into one
advance() call, so `abs(delta)` is the input layer's proxy for spin speed; at or
above the threshold the move collapses to a single blended pass — one
`TransitionStep` (the arrival edge, marked `blended`, landing straight on the
destination, `RingMove.fast=True`) instead of the full chain. Landing index,
seam-crossing (`wrapped`), and arrival edge/direction are exactly the full
chain's; only the in-between transitions are dropped.
The policy is opt-in (default off) because the pure function cannot observe
wall-clock spin speed — the simulator/firmware, which can, enables it. This keeps
all existing complete-chain behavior and tests intact. The simulator passes the
default threshold and plays the blended step at 2.5× (FAST_BLEND_RATE); a
dedicated fast-blend clip can replace the accelerated arrival-edge placeholder
when real transition media exists.
- player/ring.py: TransitionStep.blended, RingMove.fast, threshold param + policy
- simulator/clips.py: serialize fast/blended in ring_move_to_dict
- simulator/app.py: apply DEFAULT_FAST_SPIN_THRESHOLD at /api/ring/advance
- simulator/static/app.js: play a blended step at FAST_BLEND_RATE
- docs: scales design §3, USER_GUIDE, ROADMAP slice-3 note
Tests: +9 (ring policy, serializer, API). Suite 224 passed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Tuning the look by eye in the simulator (design §8) surfaced that the dark
mood pole rendered a full-frame hue-rotate(-200deg) — rock orange, trees
purple: the disorienting look rejected in session 0008, not the peaceful POC
dark_frame. Replace it with darken + slight desaturate on the video filter
plus a multiply-blended deep-blue wash (#tint, below the SVG HUD so the
overlay stays legible). Dark now reads cool/somber with natural greens, like
the approved POC look; light/left/right unchanged and confirmed peaceful.
With full tilt now tasteful on every axis, LOCK DEFAULT_CALIBRATION to unity
gains + linear variant map as a deliberate by-eye choice (not placeholders),
closing the open session-0006 knob->strength decision: knobs run 0=off..4=max,
equal Dark/Light = identity, the 5 notches map 1:1 to the 5 discrete Right
bakes. Guard the locked constants with test_default_calibration_is_locked.
Resolves design §8 open questions (calibration curve shape; grade-vs-overlay
ordering) and records the dark-grade fix. Full suite 193 passed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>