Godstrument/LIFESTRUMENT_BRIEF.md
type-two 232475e0ce 🌱 lifestrument v1.0.1 — post-ship review fixes (fable)
Three finds Opus's pre-ship review missed, all verified in-browser:
the heart-connect button was unclickable (pointer-events inherited
none from #readout), orb taps rang the 2.5s Moment bell per tap (now
a short pluck via the newly exposed Synth.pluck), and Game Day's
asleep copy leaked --sports jargon to civilians. Plus the Grimoire
now names the gentle door, and the brief gains §14: review verdict
and the v1.1 punch list (hub Cache-Control for .html, manual section,
knob double-tap, per-vibe info sheets, PWA manifest, wake lock).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:52:04 +10:00

291 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LIFESTRUMENT — build brief
> Coding instructions for a fresh Claude (Opus 4.8) session working in this repo.
> Read this whole file before writing code. Where this brief and the live code
> disagree, the code wins — verify against `viz/index.html`, `hub.py`,
> `config.json` before assuming.
## 1. What you are building
**LIFESTRUMENT** is a second, self-contained client for the existing godstrument
hub: one page, `viz/life.html`, served by the same server at
`godstrument.pro/life.html`. Where godstrument is the expert console — 80+
sources, a patchable modulation matrix, fourteen belief-system skins, MIDI/OSC/CV
out — Lifestrument is the **fun one**: only the sources about *living* (bodies,
births, weather, the sun and moon, play), a handful of one-tap **Vibes** instead
of a patch matrix, two big knobs, immediate sound, and little dopamine **Moments**
(a chime when somewhere a baby is born; a golden flash at sunset; a whistle at
kickoff). A visitor should hear something beautiful within **five seconds of
tapping one button**, and never see a cable.
Philosophy in one line: godstrument (and its ZERO mode) is *build anything*;
Lifestrument is *feel something, immediately*.
**What it is NOT**: no money (no crypto/fx/market/econ/debt — banned outright),
no transport machinery (planes, ISS), no quakes/deaths in the sound (life, not
mortality), no matrix editing, no skins, no arranger/grooves, no commune, no
MIDI/OSC/CV out, no grimoire. It links back to the full instrument for people
who outgrow it.
## 2. Non-negotiables
- **One file**: `viz/life.html`, self-contained (inline CSS/JS, no frameworks,
no CDN, no build step) — same house style as `viz/index.html`. Target ≤ 3,000
lines.
- **Zero changes to hub.py / run.py / config.json / workers.** Everything is
client-side. If you believe you need a server change, stop and leave a note
instead.
- **Do not modify `viz/index.html`** except (optionally) one additive line: a
"🌱 lifestrument" item in the right-click sky menu that opens `/life.html`.
Nothing else.
- **Mobile-first.** Design for a phone held in one hand (portrait, ~390px);
desktop is the adaptation, not the base. Godstrument is desktop-first;
Lifestrument is the opposite.
- Audio starts only after a user gesture (browser rule): one big button.
- This brief and all planning docs stay **out of the deploy** (see §12).
- Never `rsync --delete` to the box; never push `godstrument_users.db`,
`auth_secret`, or `patches/`.
## 3. Architecture (all decided — don't relitigate)
**Data in.** Connect to the same hub websocket the main viz uses. Copy the URL
derivation from `viz/index.html` `connect()` (~line 2240): localhost pages use
`ws://localhost:8765`, hosted pages use `wss://<host>/ws`, and a `?ws=`
query-param override wins. Consume only `msg.sources` frames: each key carries
`{raw, norm, label, event}`. Ignore `dests`, `routes`, `macros` — Lifestrument
has no use for the shared matrix. Keep a small per-key history ring (~120
values) like index.html does for its sparklines; the orb and meters draw from it.
**Routing.** A local mini-matrix, entirely client-side: each Vibe is a static
list of `{source, param, amount, curve}` wirings applied at audio-frame rate
(`requestAnimationFrame` is fine; the synth reads a params object). The hub's
shared routes are never touched — this also means Lifestrument works against the
**read-only public hub** exactly as well as a local one.
**Sound.** Its own compact Web Audio synth (§6). Do not try to reuse the
index.html synth — it is closure-scoped and expert-shaped. ~300 lines is enough.
**Auth.** Same invite-only accounts. Reuse the exact `/api/*` endpoints
index.html calls (read its auth IIFE, ~line 6400: me/login/signup/logout with a
session cookie) — do not invent new endpoints. Landing = one screen with the
LIFESTRUMENT wordmark, a one-line poem, login/signup (reuse the flow, restyle to
§9), and after auth one giant **begin** button that starts audio and drops you
into the default Vibe already playing.
**Local sources** (client-generated, never sent to the hub; they exist only in
the local matrix, prefixed `me.*`):
- `me.heart` — Web Bluetooth heart rate. Lift the pattern from index.html's
♥ implementation (`0x180D` HR service). Chrome/Edge/Android only — feature-
detect and show "not on this browser" honestly on iOS Safari.
- `me.tilt.x` / `me.tilt.y` — DeviceOrientation beta/gamma normalized 0..1.
On iOS call `DeviceOrientationEvent.requestPermission()` from the begin
button's gesture. This is the universal free sensor: every phone becomes a
controller with zero hardware.
- `me.shake` — DeviceMotion impulse (accel magnitude over threshold, 400ms
cooldown), an event source.
- `me.voice` — mic RMS via getUserMedia + AnalyserNode, opt-in per Vibe.
- `me.tap` — taps on the orb: sets tempo (rolling average of last 4 intervals)
and fires a pluck. Fallback "heartbeat" for iOS.
**Hub sources whitelist** (the ONLY hub keys Lifestrument may use — exact keys,
all verified present in `config.json` signals):
`clock.births`, `clock.pop`, `clock.popvel`, `world.lifeexp`, `world.fertility`,
`weather.temp`, `weather.wind`, `weather.precip`, `weather.humidity`,
`sky.day`, `sky.elev`, `season.north`, `season.south`,
`astro.moon`, `astro.harmony`, `almanac.fertile`, `almanac.waxing`,
`light.lux`, `light.flash.event`, `audio.rms`, `audio.centroid`,
`tof.near`, `tof.cx`, `tof.cy`, `hand.open`,
`sport.events_today`, `sport.inplay`, `sport.kickoff`, `wiki.rate`.
(`tof.*`/`light.*` flow when the sensor rig — or `workers/sim_esp32.py` — runs;
`sport.*` when the hub runs `--sports`. Absent keys must degrade gracefully:
a card shows a gentle "asleep" state, never an error.)
## 4. The Vibes (presets) — build exactly these eight
Each Vibe is: an emoji + name + one-line promise on a card; a wiring list; a
palette tint for the orb; and one or two "moment" hooks (§7). Switching Vibes
crossfades over ~2s (ramp old wirings' amounts to 0, new ones in). Store the
last Vibe + knob positions in `localStorage` (`ls_vibe`, `ls_energy`,
`ls_dream`).
| # | Vibe | Promise | Core wiring (source → param · amount · curve) |
|---|------|---------|-----------------------------------------------|
| 1 | 💓 **Heartbeat** (default) | *your pulse is the tempo* | `me.heart` raw BPM → tempo (clamped 50140); `me.heart` norm → brightness ·0.6·lin; no heart? `me.tap` sets tempo; kick ticks every beat, hat every half |
| 2 | 🌅 **Daylight** | *the sun plays the room* | `sky.elev` → brightness ·1.0·scurve; `sky.day` → pad warmth ·0.8; inverse `sky.day` → space ·0.6 (night opens reverb); `weather.temp` → root note (see §5 key map) |
| 3 | 🌧 **Rain Room** | *the weather writes the melody* | `weather.precip` → pluck density ·1.0·exp (rain literally plays notes); `weather.wind` → pan-wobble rate ·0.7; `weather.humidity` → wash level ·0.5; `weather.temp` → root note |
| 4 | 👶 **The Living** | *somewhere, a baby* | `clock.births` → chime rate (§7 Poisson, throttled); `world.lifeexp` norm → pad brightness ·0.7; `world.fertility` → warmth ·0.5; `clock.popvel` → drone level ·0.4 |
| 5 | 🌕 **Moon Bath** | *slow silver* | `astro.moon` → shimmer level ·0.9 (full = maximal); `almanac.fertile` → pad bloom ·0.5; `astro.harmony` → consonance (major-ness) ·0.6; tempo fixed 60, everything legato |
| 6 | 🏟 **Game Day** | *the world at play* | `sport.inplay` → percussion density ·0.9; `sport.events_today` → base energy ·0.4; `sport.kickoff` → riser+crash moment; hub without `--sports` → card asleep: "the stadium sleeps — ask the keeper" |
| 7 | 🎤 **The Room** | *your space sings back* | `me.voice` → swell ·1.0·scurve; `audio.centroid` (or local mic centroid) → filter ·0.5; `light.lux` → brightness ·0.5; `light.flash.event` → sparkle hit; `tof.near` → pluck when hand approaches |
| 8 | 🤲 **In Hand** | *tilt, and it bends* | `me.tilt.x` → filter sweep ·1.0; `me.tilt.y` → space ·0.8; `me.shake` → big hit + orb burst; `hand.open`/`tof.cx/cy` join identically when the rig is present |
Curves are godstrument's: `lin`, `scurve` (ease in/out), `exp` (hugs the floor).
## 5. The two knobs (the only "mixer")
- **ENERGY** — one macro scaling: tempo ±20%, percussion density, pluck
probability, brightness ceiling. Low = ambient wash, high = it dances.
- **DREAM** — space macro: reverb size/mix, delay feedback, pad release, detune.
Low = dry and close, high = cathedral.
Both are big thumb-friendly dials (or vertical sliders on desktop), default 0.5,
persisted. Nothing else is user-adjustable in v1.
**Key map** (for `weather.temp` → root): map norm 0..1 across
`[D minor pent, F major pent, G mixolydian(pent subset), A major pent]` roots
38/41/43/45 — cold is minor and low, warm is major and higher. All melodic
content in Lifestrument is pentatonic — the no-wrong-notes rule is what makes it
un-mess-up-able.
## 6. The synth (compact, ~300 lines)
Voices (all Web Audio primitives, no worklets):
- **kick** — sine drop 120→45Hz, 90ms, on the beat when ENERGY > 0.2
- **hat** — filtered noise burst, offbeats, level ~ percussion density
- **pluck** — 4-voice poly: triangle osc → lowpass → exp-decay gain; pentatonic
notes chosen by the active Vibe's melody driver
- **pad** — 2 detuned saws → lowpass (brightness) → slow attack/release; chord =
root + 5th + 9th; "warmth" morphs triangle↔saw mix
- **drone** — one sine + one sine at the 5th, sub register, level per Vibe
- **shimmer** — high sine cluster with slow random pan, for Moon Bath
- **chime** — the Moment sound: 2-osc bell (sine + 3.01× partial), 2.5s decay
- master: delay (0.375s, feedback = DREAM·0.5) + reverb (generated impulse:
2s noise burst with exp decay into a ConvolverNode) + DynamicsCompressor
as a safety limiter.
The params object (what wirings write into, smoothed with setTargetAtTime):
`tempo, brightness, warmth, space, wash, density, pluckDensity, droneLevel,
shimmerLevel, consonance, panWobble, swell, root`.
## 7. Moments — the ding-ding engine
A Moment = toast (emoji + one line, ~4s), a chime (pitch varies), and an orb
burst (particles in the Vibe's tint). **Cooldowns are what keep them precious**
— enforce per-moment minimum gaps:
| Moment | Trigger | Cooldown |
|--------|---------|----------|
| 👶 *somewhere, a baby* | Poisson draw against `clock.births` raw (≈4/s worldwide — draw every frame, `p = rate·dt·THROTTLE` with THROTTLE tuned so it fires ~every 1530s in The Living, ~2min elsewhere) | 12s |
| 🌅 / 🌇 *the sun rises/sets where you are* | `sky.elev` raw sign crossing (needs two frames straddling 0) | 6h |
| 🌕 *the moon is full tonight* | `astro.moon` norm > 0.97, once per session | session |
| ⚽ *kickoff — N matches begin* | `sport.kickoff` event arrives | 30s |
| 🌧 *rain has started* | `weather.precip` raw 0 → >0 | 1h |
| 💓 *new calm / new fire* | `me.heart` sets a session min/max (after 3min warmup) | 5min |
| 🎂 *your day* (v2, with natal) | — | — |
First-visit onboarding is also a Moment sequence: three toasts introducing the
orb, the Vibes, the knobs. Never a modal tutorial.
## 8. The orb (the whole visual)
One canvas, one big central orb (~55% viewport width on phone):
- radius breathes with the master output RMS; a ring pulses on each kick
- hue = active Vibe's tint; inner gradient brightness = `brightness` param
- slow particle drift around it; Moment bursts emit 2040 particles
- background: a vertical gradient that tracks `sky.elev` when available (night
→ deep navy, day → pale warm) — the page itself knows what time it is
- tapping the orb = `me.tap` (pluck + tempo in Heartbeat)
- under the orb: current Vibe name + the *live fact* it runs on, in plain words,
updated ~10s: "144 hearts born this minute" / "the sun is 12° up" /
"3 matches in play". Real numbers, human phrasing — this is Lifestrument's
version of godstrument's data-honesty.
## 9. Look & feel
Dawn, not deep space: near-white warm background in day, dusky in night mode
(driven by `sky.elev`, not a toggle), one accent hue per Vibe, big rounded type
(system stack is fine; 17px minimum body), generous spacing, cards with soft
shadows, everything reachable by thumb. No tables, no panels, no right-click
anything. The wordmark is lowercase: **lifestrument** · "played by being alive".
Keep godstrument's honesty of voice ("the ECB fixes once a day" energy) but
warmer and shorter.
## 10. Degradation matrix (build these states, they are most of the UX)
- No Bluetooth API (iOS): Heartbeat card shows "🫱 tap your pulse" mode.
- Hub unreachable: local-only mode — me.* sources still play (tilt/tap/voice);
banner "the world is quiet — playing your half".
- Not signed in: landing; signup needs an invite code exactly like godstrument.
- Sensor rig absent: In Hand uses tilt only; The Room uses mic only.
- `--sports` off: Game Day asleep state.
- Reduced motion (`prefers-reduced-motion`): particles off, orb breathes gently.
## 11. Acceptance checks (run all before calling it done)
1. `python3 -m http.server 8899 --directory viz` + open
`http://localhost:8899/life.html?ws=ws://localhost:8790` against a running
hub (`godstrument-test` launch config) → sources flow, Daylight plays, zero
console errors.
2. Kill the hub → local-only banner, tilt/tap still make sound, no errors.
3. Chrome DevTools device mode iPhone 13: layout holds at 390×844; begin button
≥ 56px tall; knobs usable with a thumb.
4. Each of the 8 Vibes: switch to it, confirm audibly distinct within 3s, no
errors, crossfade smooth.
5. Moments: fake `sky.elev` crossing and a `sport.kickoff` frame in the console
(expose a tiny `window.__lifeDebug.inject(key, val)` hook, dev-only) → toast
+ chime + burst, cooldowns respected.
6. Reload → same Vibe and knob positions return.
7. Lighthouse mobile performance ≥ 90 (it's one file with no deps — this is
easy; if it isn't, the canvas loop is misbehaving).
8. Leave one runnable self-check in-repo: `test_lifestrument.py` (stdlib only)
asserting `life.html` parses as balanced HTML, whitelist keys ⊂
`config.json` signals `me.*`, and banned prefixes (`crypto.`, `fx.`,
`market.`, `econ.`, `debt.`, `quake.`, `planes.`, `iss.`) appear nowhere.
## 12. Deploy
`viz/life.html` deploys with the normal flow (it must ship — it's product).
This brief does NOT: it is in the deploy exclusion list in `CLAUDE.md`. Deploy =
rsync git-tracked files minus that exclusion list (never `--delete`), then
`sudo -n systemctl restart godstrument` on the box, then verify
`https://godstrument.pro/life.html` returns 200 and plays.
## 13. Explicitly deferred to v2 (do not build now)
Natal/birthday integration (needs the chart caster), a shareable "postcard"
image of your session, preset remixing/saving, Lifestrument-specific accounts or
theming per user, Apple Watch workarounds, camera-based pulse detection, any
server-side anything.
---
## 14. v1.1 notes — post-ship review (Fable, 2026-07-12)
**Verdict on the v1 build**: faithful to the brief and good. One deliberate,
sensible deviation — the default Vibe is Daylight (instant sound from world
data) rather than Heartbeat (silent until a tap/strap); keep it. The
understand→build→adversarial-review process caught 21 real issues pre-ship;
this review caught three more post-ship, all fixed:
- `#heartbtn` was unclickable — it lives inside `#readout`
(`pointer-events: none`, inherited). Fixed with `pointer-events: auto` on
`.on`. Lesson for future UI inside `#readout`: opt back in explicitly.
- Orb taps rang the 2.5s Moment bell per tap (4 taps to set tempo = 4
overlapping bells). `Synth.pluck(midi, vel)` is now exposed; taps pluck.
- Game Day's asleep copy leaked `--sports` CLI jargon; now says "ask the
keeper to open it". House rule: no flags, keys, or code words in
Lifestrument copy — civilians only.
- The Grimoire (index.html, after the zen bullet) now mentions Lifestrument as
"the gentle door". Keep that in sync if the URL or pitch changes.
**Follow-ups worth doing (v1.1, small):**
1. **Hub `Cache-Control`** — the hub serves HTML with no cache headers, so
browsers heuristically cache; after every deploy some users run a stale
page until a hard refresh (bit us during this very review). Add
`Cache-Control: no-cache` for `.html` responses in hub.py's HTTP handler.
Tiny, safe, helps godstrument too. (The v1 "zero server changes" rule was
for the build, not forever.)
2. **Manual** — GODSTRUMENT_MANUAL_SOURCE.md needs a Lifestrument section; a
background task may already be adding Earth Echo — check before editing to
avoid a collision.
3. **Knob double-tap** resets to 50%.
4. **Per-vibe info sheet** — long-press a vibe card → one honest paragraph on
what it listens to (the data-honesty ethos, one tap deeper).
5. **PWA manifest + icon** so phones can install it (needs a small manifest
file next to life.html; the single-file rule can bend for a manifest).
6. **Screen Wake Lock** while playing (optional; audio survives screen-off,
but the orb doesn't).
**v2 list in §13 stands.** If sports monetizes, remember `sport.goal`
(see the sports-source memory) would make Game Day genuinely great.