A new map-layout skin — the world's own sensor picture read AS the cosmology,
ported from the GODSIGH view. A minimal equirectangular Earth (a sparse hand-drawn
continent set kept inline — no heavy geo data in the one-file console), a 30°
graticule refit to a 2:1 rect each frame (dynamicStations), and nine geographic
"posts" the sources are seated at by godseyeOf. The overlay seats every feed at its
LITERAL coordinates: earthquakes flash at quake.lat/lon (venue-antipode fallback if
absent), the ISS and recon birds sweep as a moving point + trail from sats.iss_lat/
lon, aircraft are a shimmer band, the markets pulse at NY/London/Tokyo, the solar
wind washes the poles as an aurora, the word sparks at civilisational coordinates,
and your own hands are the ring at the venue. Cold watch-floor palette (near-black,
cyan, warning amber) and a Bb minor-pentatonic retune. Registered in SKINS +
SKIN_HOME beside the bagua; grimoire + manual entry ("the eye that hears").
To give the console the venue's location (it had none), world_sky now emits
/gs/sky/lat,lon; config.json declares sky.lat/lon and quake.lat/lon so the eye can
place "here" and the quakes. (sats.* signals + routes already landed with world_sats.)
Verified: full viz parses/runs with the groove self-check passing and zero console
errors; an isolated harness renders the REAL overlay code into a coherent world-map
with every layer; and an end-to-end run (feeds -> hub -> WS) delivers all the eye's
input signals. Adversarial review found one wrap bug — the ISS trail streaking
across the map at the ±180° antimeridian — now fixed by breaking the polyline at the
seam (verified: a seam-crossing sine trail renders split, no full-width streak).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reader side of the OpenSky shared-cache v1 contract (mirrored in GODSIGH
SPEC2.md §4). Path ~/.cache/godverse/opensky-states.json (env OPENSKY_CACHE_FILE),
content the raw global /states/all body, freshness = mtime within 120 s.
Each poll cycle now checks the cache first: on a fresh hit it reads the global
body, filters the states to our bbox client-side (indices 5=lon, 6=lat), and
skips the HTTP call entirely (logs "cache hit"). On 429 or a network failure it
falls back to a stale cache rather than nothing. This worker only ever polls a
bbox, so it never WRITES the cache (never partial data into a global-contract
file) — it is a pure reader, and degrades to its old ≥450 s bbox fetch when no
writer is around (i.e. prod). Result: godstrument + GODSIGH running together
spend ~one poller's quota instead of colliding and 429-ing each other out.
Verified: unit tests for read_cache freshness/stale/missing + bbox filtering,
and an integration run where a fresh cache is served (count filtered to bbox,
zero HTTP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sky's own schedule joins the orchestra. Unlike world_iss (which begs a web
API per position), world_sats fetches TLEs once from Celestrak — the stations
plus the named recon birds GAOFEN and COSMOS — caches them to
~/.cache/godverse/tles.txt (24 h freshness, atomic writes, stale-on-failure),
and propagates every craft locally with SGP4 at any rate, no per-fix HTTP.
For the venue city (--lat/--lon, retargeted by run.py) it computes each craft's
elevation above the horizon and emits:
/gs/sats/best_elev highest elevation among all tracked (-30..90 deg)
/gs/sats/overhead_count how many above 10 deg
/gs/sats/iss_lat/iss_lon ISS sweep (available even without world_iss)
/gs/sats/recon_elev best elevation among only the GAOFEN/COSMOS watchers
/gs/sats/pass.event impulse when any craft crosses rising through 30 deg
Wiring: config.json declares the six signals with norm ranges + labels, three
starter routes (best_elev -> pad.brightness, pass -> delay.feedback,
recon_elev -> lfo.rate) and adds sats.best_elev to the cosmos group; run.py adds
the worker to the WORLD profile and the lat/lon retarget set; requirements notes
the pure-python sgp4 dep. The worker is import-safe (sits out if sgp4 is absent)
and exits politely with no cache and no network. Grimoire + manual entry added.
Math ported from GODSIGH's verified satellites layer: TLE -> Satrec.twoline2rv
(handles no_kozai), SGP4 -> TEME km, GMST rotation -> WGS-84 geodetic, then an
ECEF up-vector for elevation. Verified end-to-end against live Celestrak data:
98 sats tracked, ISS position/altitude plausible, and all signals arrive over
the hub WebSocket correctly normalized with their routes active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feature-gap analysis of local/groove-editor (c0c3a98) vs current main showed
godrum independently built almost everything the branch had — per-step
velocity/chance/ratchet, euclid, swing/humanize, scale/root, Play-One stacks,
Follow Actions. The one genuine, valuable gap was NOTE LENGTH (the "key-roll"):
main's notes were all one-16th. Ported that natively; skipped the rest as either
already-covered, a deliberate divergence (global scale, Play-One vs polyphony),
or too invasive to land without regressing godrum (clip-length/polymeter would
rewrite the fixed-16 architecture that migrateSeq/arranger/beat/commune assume).
What landed (all back-compat; len defaults to 1 = one 16th, so old patches are
unchanged except un-ratcheted sequenced notes now gate to their drawn length —
which is the feature):
- len[16] on seqFor/drumSeqFor + migrateSeq back-fill + two self-check guards
- attachNoteLengths(): .seqnote overlay blocks with an ew-resize drag handle,
used by the voice-inspector roll and the arranger grooves-tab roll; len is
per-voice (shared across A/B/C clips) exactly like vel/chance/rat
- playback: seqTick gates rat==1 notes to len*sixteenth; Synth.pluck gains an
optional durMs (null = byte-for-byte the old natural decay); ratchets unchanged
- MIDI: export emits length-scaled note-offs with a same-pitch overlap clamp;
live midiPluck now tracks pending offs per (channel,pitch) so a held note
retriggered on its own pitch ends cleanly instead of being cut by a stale off
- grimoire + regenerated manual
Verified: groove self-check passes (incl. len guards); an isolated harness
confirms overlay render/drag/clamp at both grid sizes + Play-One stacks + the
getter that keeps overlays in sync when the arranger swaps sq.steps per bar.
The reference branch local/groove-editor is the only copy of this work and is
deleted now that note-length is landed and pushed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 4 docs. Four entries added to the grimoire's living-patch list, alongside
the groove-grids/tracks/Groove entries, in the house voice:
- 🥁 the beat — five drum lanes on the godtime clock; draw/▶; velocity/chance/
ratchet; ⬢ euclid; the feel shared with tracks; the 🎛 kit drawer incl. duck
("the kick parts the sea"); saves with the patch; moods never silence it; a
beat drawn in zero counts as a build.
- keys 1–5 & ⏺ rec — finger-drum the kit (⇧ ghost); rec quantizes taps to the
nearest sixteenth.
- 🌾 AMPLER — "there is ample, for god is abundant": capture the sounding world
(a fed tab counts), chop 8/16, audition, ⇄ reverse, seat slices onto voices;
slices ride level/pan/duck/choke; session-only.
- the beat leaves the building — GM drums on MIDI channel 10 (36/38/39/42/46)
and a ch-10 track in export .mid.
viz/manual.html regenerated via build_manual.py (27 sections, self-check ok).
GODSTRUMENT_MANUAL_SOURCE.md left untouched (viz/index.html is canon).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 3 / Agent E — the performance layer.
- fireDrum(name,vel,durMs): one shared fire path for the sequenced beat and
the played keys; seqTick's drum branch routed through it (behavior identical).
- Keys 1–5 play kick/snare/clap/hat/ohat while the beat panel is open (vel 0.9,
⇧ = 0.5 ghost); ev.code with an ev.key fallback for code-less environments.
- ⏺ rec toggle (beat.rec, red when armed): each tap quantizes to the nearest
16th (round seqPhase % 16) and writes steps[n]/vel[n], repaint via beatRefresh.
- exportMid: a channel-9 GM drum track (36/38/39/42/46), the beat looped across
the arrangement's bars, one 16th long; ratchets omitted in v1. Added before the
bail check so a beat-only export still produces a file.
Territory: openBeat header, keydown, seqTick, exportMid + one #beat CSS rule.
Nothing inside the Synth IIFE; no ctx-menu entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SP-404 move, Godstrument-style. GODSONIQ already captures ~3s of the
live master; AMPLER routes its own capture into a separate amplerBuf
(recTarget-tagged per-capture, so GODSONIQ's keys-replay stays byte-
identical), chops it into 8 or 16 equal slices, reverses any slice (lazy
cached copy), and seats a slice onto any drum voice. An assigned voice
PLAYS its slice through that voice's level→pan→drumBus chain, so the kit
knobs and patch persistence keep acting; a slice-kick still ducks and a
slice-hat/ohat still obeys the TR choke (both factored to be source-
agnostic). Eight frozen ampler* methods + an amplerPeaks waveform helper.
UI: a right-side #ampler panel (capture · waveform · chop · slice grid
with per-slice reverse · per-voice assignment strip), opened by one sky-
menu ctxItem after GODSONIQ. Engine lives in the Synth IIFE; the panel
sits after openBeat's brace — E's territory (openBeat body, seqTick,
keydown, exportMid) untouched. AMPLER state is session-only in v1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four defects from the independent 4-lens adversarial review of Stage 2.
R8 choke same-task noise pop — DEVIATION from the brief's fix, verified by
measurement. Read-before-cancel does NOT work: reading gain.value right after
setValueAtTime(0,t) in the same task returns the 1.0 default (the envelope
hasn't rendered), both offline AND live — so the anchor is 1.0 and it still
pops (measured 0.45 vs a 0.15 ohat). Correct primitive is cancelAndHoldAtTime,
which holds the timeline value at t (0 same-task, decaying level mid-ring);
read-value kept as the legacy fallback. Same idiom applied to fireKick's duck.
Verified live: same-task double-ohat 0.152 ~= single 0.149 (no pop); normal
different-task choke still cuts the tail 70%.
R9 euclid junk input — trim, split /[\s,]+/, drop empty tokens, Number(), bail
unless the first token is finite. " 4"/",4" -> E(4,16); "abc"/"" -> untouched;
"0" -> cleared; "5,2" -> E(5,16) rot 2. (Empty-token filter also fixes the ""
vs "0" distinction a bare Number() can't.)
R10 prototype pollution via kit data (MAJOR — patches/vibes are cross-user) —
own-property guard (hasOwnProperty) everywhere a caller-supplied name indexes
KIT / N.drums / DRUM_DEFS: drum, drumParam, drumParamList, drumKit, drumKitLoad,
drumSync. Verified: drumKitLoad({"__proto__":{toString:0.5}}) and
drumParam("__proto__",…) leave Object.prototype intact; legit keys still apply;
normal round-trip works.
R11 a beat is a build — hoist the canonical seed into DRUM_SEED (one source of
truth for seedBeat + zeroHasBuild); zeroHasBuild() now returns true when any
drum lane is on or its gates differ from the seed. Untouched seed still exits
quietly; ▶ or a drawn step now shows the keep/weave card so the beat survives.
Temporary verify hooks removed (grep-clean). No merge to main, no deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 2 (Agent C). Real merge of godrum/a-engine + godrum/b-beat-panel onto
godrum/integrate (zero conflicts; extracted app script passes node --check),
then the seven review findings:
R1 kick tail — stop the osc at t+0.01+8τ (τ computed once); no click at max decay.
R2 seed — clear closed hats on steps 14/15 so the seeded open hat rings into the downbeat.
R3 stale paint — paintGrid now drives paintExpr (lit-bars track the grid); groove-slider drags relight the preset buttons.
R4 groove — add the fourth "velocity" slider (groove.velo), matching the arranger's grooves tab.
R5 moods — applyMood's storm + clearGrooves loops skip drum.* lanes; a mood never silences the beat.
R6 kick-duck — duck param on the kick (default .35); pad+drone reroute through a new duckG; fireKick dips it and lets it flood back ("the kick parts the sea").
R7 mix — closed/open hat internal peak scalars 0.70/0.80 → 0.35/0.45 so the hats sit at/just under the ambient perc.density shimmer (§0.6 contract defaults unchanged).
Verified end-to-end on a static serve via offline-render measurements plus a live
analyser tap on the real graph: first-press wake with ♪ off, every voice audible,
choke (95% tail cut), duck −3.07 dB → recover, chance/ratchet, R5 through a storm,
kit+beat persistence incl. duck, zero-mode round-trip, arranger↔beat shared groove,
mutual-exclusion, zero console errors. Temporary verify hooks removed (grep-clean).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kick / snare / clap / closed-hat / open-hat as one-shot node graphs
summed onto a drumBus → preSat, so drums ride the world's saturation →
crush → chorus/reverb/echo like every other voice. Each voice keeps a
persistent level→pan chain; hits connect into it. TR-style choke: a
closed hat OR a new open hat cuts the ringing open hat.
Frozen API (§0.6): drum(name,vel), drumParam get/set, drumParamList,
drumKit / drumKitLoad. All params normalized 0..1 at the boundary and
mapped to real units inside each voice; non-finite inputs rejected.
Entirely inside the Synth IIFE — no other territory touched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app no longer announces GODSTRUMENT to someone already playing it —
the landing page and auth modal keep the brand; the console keeps only
the counts and the header line. The grimoire trigger slides up into the
vacated corner, and drawSpacedText goes with the wordmark it existed for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
On production /api/me fails when signed out, so setUser was never called
and the auth form (hidden by default) never appeared — a dead landing.
Default authArea to visible; setUser still swaps to the begin button
once signed in (or via ?dev=1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second, self-contained client for the same hub, mobile-first: only
the sources about living (bodies, births, weather, sun & moon, play),
eight one-tap Vibes instead of a matrix, two macro knobs (ENERGY /
DREAM), a breathing orb, and dopamine Moments (somewhere a baby, the
sun where you stand, a kickoff). Its own compact Web Audio synth, all
routing client-side so it plays against the read-only public hub.
Reuses the invite-only /api auth; local sensors (heart BT, device tilt,
shake, mic, tap) never touch the hub. No money, no machinery, no
cables. Links back to the full instrument; added a "🌱 lifestrument"
item to godstrument's sky menu. Built understand→build→review with an
adversarial pass; 21 findings fixed (a11y sliders, zoom, contrast,
warm-dawn background, reachable heart connect, pluck de-click,
scheduler catch-up cap, moment tuning). test_lifestrument.py guards the
whitelist and the money/machinery ban.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second client for the same hub: viz/life.html, mobile-first, life
sources only (no money, no machinery), eight one-tap Vibes, two knobs,
a breathing orb, and Moments — somewhere a baby, sunset where you
stand, kickoff. Godstrument is build-anything; Lifestrument is
feel-something-immediately. Brief added to the deploy exclusion list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 13-agent adversarial whole-app sweep (client controls / runtime / server-auth /
new features / feed-config-honesty, each finding reproduced-or-refuted) found ZERO
broken functions — verdict healthy. Fixed the reachable low-severity edges it did
confirm:
- viz/index.html:1804 — arranger 'grooves' tab threw TypeError (nSel.isNote) when
dests is empty in the cold-connect window (post-sign-in, pre-WS-hello). Guard:
dests.get(arrSelKey) || {isNote:false} (the 'clips' tab already tolerates empty).
- viz/index.html — picking a skin from the sky menu / F4 next-skin / F4 prev while
in ZERO mode force-enabled an invisible, un-toggleable chakraMode (every clear
path is zeroMode-guarded). Added the same !zeroMode guard to all three sites
(byte-identical outside zero — no regression).
- hub.py:117 — GODSTRUMENT_READONLY parse treated any non-empty non-'0' value as
true (so 'false'/'no'/'off' enabled read-only). Now an explicit truthy set
{1,true,yes,on}; prod uses '1' → stays read-only (verified before deploy).
- viz/index.html:2996 — removed a dead else branch in zero 'save as vibe' (its
'vibes land in the next update' placeholder was unreachable; saveVibe is a real
sibling method) → call this.saveVibe(gk) directly.
- transform.py:40 — collapsed a no-op ternary (both branches float(v)).
- viz/manual.html — regenerated (was stale vs build_manual.py; picked up the
admin-panel CSS added in 764e28b; grimoire prose was already in sync).
Left as documented cosmetic: sanitizeVibe caps emoji at 2 code points (can split a
ZWJ/flag glyph on a shared vibe — display only, no XSS). Verified: node --check,
all .py compile, auth --selftest green, app boots into zero clean, console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fable flagged that stateless session tokens bound only to uid+expiry — so an
admin action couldn't actually invalidate a live 30-day cookie. Reproduced 3
facets, all one root cause, all fixed by binding the token to the account's
credential state:
- make_token/read_token/_bind: token = uid:exp:bind:sig where
bind = hmac(secret, 'uid:created:salt')[:16]; read_token re-derives it from the
user's CURRENT row and rejects on mismatch/missing. So a token dies on account
DELETE (no ghost session/writes), password RESET (salt rotates → admin can truly
lock someone out), and SQLite id-REUSE (different created → no identity takeover).
Old 3-part tokens fail to parse → a one-time re-login. No schema change.
A follow-on adversarial review (4 security lenses × find→refute×2, GO gate) then
surfaced 3 low-severity issues, all fixed here too:
- self-de-admin guard used (identity) — {admin:0}
/ '' / 'false' / [] slipped past and could strand a sole db-admin. Now only a
real JSON bool toggles admin (closes the bypass AND the truthy-string mis-grant).
- signup invite claim was check-then-write (double-spend under concurrency) — now
an atomic +
rowcount check; signup/admin-edit wrap the UNIQUE writes in try/except
IntegrityError → friendly message instead of a 500.
- login-CSRF (Lax cookie only): hub.py _api now blocks cross-site mutating requests
by Origin (allowlist godstrument.pro/localhost + Host-match fallback so legit
traffic always passes; safe GETs never blocked).
Verified: auth.py --selftest (delete/reset/id-reuse kill the token; guard bypass
closed for every falsy value); live curl (CSRF 401/401/401/403, GET never blocked);
real-browser flow (new-format cookie authenticates through a page load, admin table
renders, same-origin admin POST passes CSRF). Console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin panel could mint invite codes and read stats but not manage the people.
Adds, all under the existing admin-gated /api/admin/ block (default-closed:
no session → 401, non-admin → 403):
- GET /api/admin/users — every account: id, email, admin flag, patch count, joined
- POST /api/admin/user/<id> — rename / change email / grant-or-revoke admin
(validated like signup: format + uniqueness)
- POST /api/admin/user/<id>/reset — set a fresh random temp password, returned ONCE
(never stored plaintext or logged) to relay to the user
- DELETE /api/admin/user/<id> — delete the account + its saved patches (feedback +
used invite kept as history)
Self-lockout guards: you can't delete your own account or revoke your own admin.
Admin granted via the GODSTRUMENT_ADMIN env is marked env_admin and its toggle is
hidden (can't be removed in the db). auth.py migration-free — uses existing tables;
the is_admin column was already migrated.
Client: the ⚙-account admin panel now lists every user with inline edit (username +
email), 🔑 reset-pw (shows a copyable temp), ★ make/revoke admin, and 🗑 delete
(confirm); the panel widens for the table. Names/emails escaped on render.
Verified: auth.py --selftest (list/edit/reset/delete + endpoint guards); live curl
against the local dev db (rename, bad-email/dup rejected, admin toggle, self-guards
400, non-admin 403, delete cascades presets); and the UI end-to-end (rows render
with correct per-row actions, edit expands, reset shows a temp password). Console
clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public site only had the in-app grimoire (behind the ✦ overlay); there was no
shareable, linkable user manual. build_manual.py extracts the grimoire (the canon,
per CLAUDE.md) + its styles from viz/index.html into a self-contained, always-
visible page with a generated 27-section table of contents and a 'play the
instrument →' back-link. Single source → no drift; re-run after editing the grimoire.
Served at godstrument.pro/manual.html (docroot is viz/). Verified: renders as a
normal page (overlay/close-button overridden away), TOC anchors resolve, all
feature sections present (zero, OMNI panel, vibes, dimensions), console clean.
Also refine the deploy convention: exclude dev docs/scripts (CLAUDE.md, README,
ZERO_OMNI_BRIEF.md, GODSTRUMENT_MANUAL_SOURCE.md, godstrument.txt, build_manual.py,
tests) so working notes never land on the public box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fix-verify pass flagged one CONFIRMED regression in the #1 floor loop: it
pinned any tweaked cableless non-note dest to a hard 0, but two dests have a
non-zero synth idle — filter.cutoff (d(...,0.3)) and tempo.nudge (d(...,0.5)).
So touching the filter knob in zero (or, before this, merely opening OMNI, which
created a neutral tweak) pinned filter.cutoff to 0 → 250 Hz, darkening the lead.
- new DEST_IDLE map; the floor loop seeds destVals[k]=DEST_IDLE[k]||0, so a
neutral/reset knob returns a voice to its idle (filter 0.3), an offset rides
from the idle, and mute still → 0. (verified: reset filter → 0.3 not 0;
drag up → 0.767; mute → 0)
- OMNI update() + knob pointerdown now read tweaks[k] directly instead of tw(k),
so rendering/opening the panel no longer creates phantom neutral tweaks (which
also kept the tweaks map — and saved dimensions — clean). (verified: open OMNI
→ filter has no tweak)
A clean instrument (no tweaks) stays byte-identical; the public house patch was
already masked (filter.cutoff is cabled to sun.speed). Deploy gate: GO.
Console clean, syntax clean, 21 knobs, field healthy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 30-agent adversarial review (5 dimensions × find→refute×2) confirmed 8 defects;
all fixed and live-verified:
BLOCKERS
- #1 unwired OMNI trim couldn't be silenced: the offset-floor only seeded a dest
while offset>0.0015, so reset/mute/lower stopped emitting the key and the last
floored value stuck in the synth (Synth.update merges). Now ANY tweaked cableless
non-note dest is seeded 0 so the processing loop writes its real value, incl. 0.
(verified: crush norm 0.5 on drag → 0 on reset)
- #2 leaving zero with an OMNI/offset-only build (no cables) was silently discarded
— exitZeroToMood gated on routesArr.length===0. New zeroHasBuild() also counts
picked orbs + any non-neutral tweak, so the keep/old/weave card always shows.
(verified: offset-only build → 'you shaped this instrument by hand. keep it?')
- #3 vibe scale whitelist used truthiness (SCALES[q.scale]) → 'constructor'/'__proto__'
passed → NaN into a Web Audio AudioParam → threw ~40×/s (remotely triggerable via
public #vibe= links). Now Array.isArray(SCALES[...]) + quantize only on note dests;
mxQuantize fallback hardened too. (verified: fx quantize dropped, note scale→minor_pent)
SHOULD-FIX
- #4 finishZeroExit keep/weave clobbered the kept mix (applyMood resets MOOD_KEYS):
re-lay the build's tweaks after applyMood; weave merges build tweaks over prezero.
- #5 crush knob was fully dead (never in dests): OMNI.render ensureDest()s every
voice/FX key → arc lives, floor works, right-click opens the menu. (verified)
- #6 OMNI drags in zero weren't persisted: knob pointerup/dblclick call zeroUI.persist()
when zeroMode → survives reload. (verified: offset 0.467 in gs_zerobuild)
- #7 reloaded vibes showed OFF: persist activeVibes + per-route _vibes/_base to
gs_zerovibes, restoreVibeState() on enterZero(true). (verified: vibe restores ◉)
- #8 GODSONIQ chip lit ~3s late: update() re-lights engine/♪ chips each frame.
Console clean, syntax clean, 21 knobs, field healthy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vibe name + emoji arrive from user-pasted GSV1 codes and #vibe= URL fragments
and are rendered via innerHTML in the vibes panel + the shared-vibe offer card.
name was <-escaped but emoji was raw. sanitizeVibe now strips [<>&"'`] from both
at the trust boundary, so nothing dangerous reaches any render path.
Verified live: a crafted code with name='<img onerror=…>' + emoji='<svg onload=…>'
imported via #vibe= → no <img>/<svg> in the DOM, no script fired, both rendered as
inert text. (ship-check item 3: user strings → HTML must be escaped.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Canon (viz/index.html grimoire) is truth; manual synced to match.
- grimoire: two new tech-manual sections in the OMNI/Earth-Echo/GODSONIQ style
(numbered how-to + spec table) — 'Building from zero — the empty instrument'
(rings → orbs → drag/tap routing → synth-roll → keep/weave exit → persistence)
and 'OMNI — the synth panel' (live arcs, trim-riding knobs, engine selector,
earth echo, transport). Rewrote 'Saving your instrument' as dimensions & vibes.
- manual (had drifted — still said MY PATCHES): added Building-from-zero + the
🎹 OMNI panel; 'Saving' → dimensions & vibes; keymap now lists F11 zero + O.
Verified: grimoire renders all three new h2 sections, no broken markup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 🎹 OMNI button (sibling of 🎛 tracks, key O) opens a floating panel — the
planet's synth, laid out like a synth. NO new audio paths: a view over the same
dests + tweaks the field already drives.
- knobs: each knob's ARC is the dest's live value (n.normDisp, the same number
the field meters — it breathes with the world); dragging rides that dest's
tweak OFFSET (setParamLocal) and a dot marks your hand; double-click resets to
neutral; right-click opens the node's full context menu. Note voices (lead/bass)
show a live pitch arc — display-only (their character is the engine selector).
- modules: VOICES (lead/bass/pad/drone/perc), FX RACK (filter/reverb/delay/
saturation/glitch/granular/crush/morph/lfo/space), 🌀 EARTH ECHO (a toggle;
its six echo.* knobs appear only when it's on).
- header: voice-engine selector (FM tine / 🎸 string / 🎛 GODSONIQ — the pluck's
character, via Synth.stringVoice/godsoniq/sampling), ♪ start-stop, live BPM,
🔒 godtime lock.
Matrix honesty fix (computeDests): a hand-set positive offset now gives a voice a
floor even with no cable — so an OMNI (or the existing 'trim') knob can make a
voice audible from nothing. Only affects dests a user explicitly offset; a clean
instrument (all offsets 0) is bit-identical to before.
Verified live (z0test, via a manual matrix pump since the headless preview's rAF
is paused): panel opens with 21 knobs + 3 modules; idle arcs track live data
(filter 0.41); dragging reverb up floored its value 0.09→0.59 with the tweak dot
showing; double-click reset it; engine selector swapped FM→string; earth-echo
toggle revealed its knobs; right-click opened the 'filter cutoff' node menu.
Tweaks persist in the saved dimension (serializePatch carries tweaks). Console
clean, syntax clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A dimension is everything (the whole scene: routes, tweaks, grooves, tempo);
a vibe is a feeling you can hand a friend. Same /api/presets storage — this is
a framing rename, existing saves appear unchanged.
- ⚙ panel: 'MY PATCHES' → 'DIMENSIONS', copy reworked; input placeholder
'name this dimension…'.
- the load dropdown always leads with '✦ dimension zero — the empty field';
loading it calls enterZero() (the place you build from nothing).
- grimoire 'Saving your instrument' rewritten (canon = truth): the dimension /
vibe distinction, the GSV1 share code + #vibe= link, 'build a vibe, save a
vibe, share a vibe'.
Verified live (z0test): panel shows DIMENSIONS + ✨ VIBES, MY PATCHES gone,
dropdown leads with dimension zero. Console clean, syntax clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A vibe = a small bundle of cables {v:1,name,emoji,routes[≤32]}. Distinct from a
dimension (the whole scene): a vibe is a feeling you can hand a friend.
- apply is additive + reversible: routes a vibe creates are tagged (route._vibes)
so toggling it off removes exactly those; a route two vibes share survives until
both are off; a hand-built cable (route._base) a vibe merely rides is NEVER
deleted. Tags live in-memory only — they don't leak into saved patches.
- save from a zero orb (right-click → 💾), from any node's right-click in the full
field (💾 save these as a vibe), or 'save current wiring' in the panel (≤32 cap).
- ✨ vibes panel (zero pill + ⚙ gear): list, toggle on/off, ⧉ copy share code,
× delete, paste-to-import. localStorage library (gs_vibes).
- share: GSV1.<base64url(json)> clipboard code + godstrument.pro/#vibe=… fragment
that OFFERS on load (add it / just save) and never auto-applies.
- trust boundary: sanitizeVibe hard-validates every import — unknown source/dest
skipped (not thrown), amounts/roots/octaves clamped, curves/scales whitelisted,
32-route cap, try/catch the base64/JSON. Garbage codes are rejected cleanly.
- per-account cloud storage deferred: the code + local library already deliver
build/save/share across devices and friends. (add a vibes table if users ask.)
Verified live (z0test): built a 2-cable orb → saved 'sunrise duo' (quantize
preserved) → panel listed it ◉ → share code round-tripped through import →
garbage code rejected → hard-reload kept the library → toggle ON created exactly
2 routes (lead.note lit), toggle OFF removed them → #vibe= fragment offered
'shared gift' in the full app, hash cleared, not auto-applied. Console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: leaving zero with a build no longer silently discards it — a compact
three-way card (keep my build / bring the old world back / weave them
together); esc stays in zero. 0-route exits stay quiet (restore prezero).
C2: the zero build survives a reload — picks + cables persist to
gs_zeronodes/gs_zerobuild; enterZero(true) on the reload path restores them.
C3: tap-to-patch (arm an orb, tap a voice) as a touch fallback for drag;
right-click an orb → member list (× to drop a feed + its cables),
unpatch-this-orb, and a save-as-vibe stub (Z3); a ⛈ enter-the-storm pill.
Nit: the chakra-view key/menu is a no-op in zero (nothing to draw there).
Verified live (z0test): pick→orb→tap-patch lit lead.note; hard reload restored
the ☀ orb + lit socket; the exit card's keep-my-build landed in a healthy full
field on the storm mood with zero-storage cleared; orb menu drops members.
Console clean, syntax clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The functional heart — build the instrument by hand from zero.
- Orbs are draggable; the output-rack sockets are drop targets. Drop an orb on a
voice/FX socket → lays a cable per member feed (registerRoute), auto-adding a
default quantize (minor_pent / root 60 or 36 for bass) when the target is a
note voice. The socket lights green when it carries routes; a ticker line
confirms the patch and points to ♪.
- Right-click a socket → the "synth roll" assign menu: for note voices a scale
(the 11 SCALES) / root / octaves picker; for CC voices an amount slider + curve
picker (lin/exp/exp3/log/scurve/inv). Plus "× unpatch all". Edits the live routes.
- refreshSockets() reflects existing routes on open; hover-highlight on drag.
Z-AC verified live: pick sun.speed (cosmos ring) → drop on filter · pick
crypto.price (wealth ring) → drop on lead → both sockets light · right-click lead
→ scale picker (11 scales, root, octaves) · press ♪ → OMNI plays (button green).
Bitcoin sings through a sun-opened filter, built from nothing. Console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each built group becomes a spinning orb on the stage, not a flat chip — a glowing
core in the group's hue with its picked feeds as petals orbiting around it (CSS
zspin/zpulse). They accumulate as you build: pick from fire → one orb; add cosmos
→ two; and on. Orbs breathe live — the core glows by the group's average value,
each petal brightens with its own feed. More feeds widen the orbit ring.
Verified live: fire (3 petals) + cosmos (2 petals) render as two spinning glowing
orbs; console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The overlay that replaces the sensory flood. In zero mode the node field is not
drawn (frame() skips drawCables/drawSourceNode/drawDestNode); a DOM overlay
takes its place over the starfield:
- 7 vibe-rings down each side — the 14 config groups (left = elements/feelings:
light·dark·fire·water·earth·air·spirit; right = domains: planet·cosmos·heavens·
human·market·wealth·summer). Each: a hued dot (GROUP_META — taste consts,
retune freely), label, live feed count, and a breathing glow scaled by the
group's live average value. Members come from groupsInfo (the hub's config).
- Picker drawer: click a ring → it slides in from that side listing the group's
feeds (name, key, live value bar, ⊕ that toggles in place). Pick 1+ → a stage
node (chip, colored by group) lands on the center stage.
- Output rack along the bottom: OMNI voices (lead·bass·pad·drone·perc) + space &
grit (filter·reverb·delay·…) as labeled sockets. Visual only — routing is Z2.
Veil (z-8) sits below the ♪/tracks/grimoire controls (z10-13) so they stay
usable. Verified live: rings render with hues + live glow, heavens drawer opens
with its 5 feeds + value bars, picking lands stage chips, ⊕ toggles in place,
console clean. Routing to the rack + right-click assign = Z2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calm front door. Zero is not a muted world (moods do that) — it's an EMPTY
one you build by hand, the same matrix started from nothing.
- enterZero(): snapshots the current patch → gs_prezero, then loadPatch({}) empties
the field; sets clientMatrix so the hub can't re-populate the cables; gs_mood="0 zero".
- exitZeroToMood(name): restores gs_prezero (the full factory) then applies the mood —
leaving zero brings the whole world back.
- First-visit choice card ("one world, three doors"): 🌱 start from zero · 🌅 first
light · ⛈ the full storm. Replaces the silent default-into-first-light; backdrop/esc =
first light. Returning users keep their last mood; a returning zero-user re-enters zero.
- Mood submenu gains "0 zero — build from nothing"; F11 toggles zero (KEY_ACTIONS).
Verified live (logged-in preview): card renders with 3 doors; "start from zero" empties
the field (56 factory cables cleared, snapshotted); F11 restores all 56 + first light
(92 BPM locked); returning-zero reload re-enters zero with no card; console clean.
The 7+7 calm slot UI is Z1. Not deployed yet (deploy is Z6, post-review).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Names the built-in browser synth OMNI (♪ tooltip, intro prose) and adds three
scannable "tech manual" reference sections to the grimoire — OMNI (full voice
roster + how-to), the Earth Echo (six world-driven tape knobs + Schumann), and
GODSONIQ (resample + tab feed) — each a numbered how-to plus a two-column
control/dest spec table (new .spec / ol.steps CSS; auto-indexed in the TOC).
Every label, dest key and world→voice mapping extracted from code and verified.
Fixes two drifted claims in the old Earth Echo prose the audit caught: tape
wear drives hiss only (grit/saturation is static, not world-driven), and the
Schumann sub is ×4 = two octaves (not "four times").
Manual: sections flow into PART I (extractor taught .spec/.row → markdown
bullets) + an OMNI umbrella entry in Appendix J.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-click sky → "🌐 feed a tab": getDisplayMedia({video:true, audio:{…}})
lets the user share another tab's audio (a YouTube tab, a stream). Wired via
createMediaStreamSource into preSat → the full FX chain + master, so it's
audible live, crush-able, and resampleable by GODSONIQ from one wiring. Video
track stopped immediately; a track 'ended' listener handles Stop-sharing.
Audio constraints (verified against MDN/W3C/Chrome docs): suppressLocalAudio
Playback so the tab isn't heard twice, and echoCancellation/noiseSuppression/
autoGainControl off so the WebRTC voice-DSP doesn't mangle music. Chromium-only
(feature-detected; graceful ticker hints for unsupported / no-audio-track);
macOS grabs a tab not all-system; DRM services capture silent, YouTube fine.
Grimoire (truth) + manual Appendix J updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record ~3s of the live master mix into a buffer via a recorder AudioWorklet
tapping the full post-FX bus, then replay it pitched across the keyboard
(playbackRate = 2^((midi-60)/12), middle C = unity). Runs through the same
FX chain as every voice, so `crush` gives it the classic 12-bit sampler grit.
Right-click sky → 🎛 GODSONIQ to sample / re-sample; 🎸 string voice exits
back to the FM tine. Capture path verified: full-amplitude sawtooth recorded
clean (peak 0.98, rms 0.49) over the realtime port round-trip.
Grimoire (truth) + manual Appendix J updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last two names that lied are now true:
- wavetable.morph — was a filter-Q sweep. Now a REAL wavetable: the lead is two
band-limited PeriodicWaves (a mellow one + a bright saw-rich one) crossfaded by
morph. Spectrally verified: smooth brightness sweep (centroid 462→880→1174
across morph 0→0.5→1), no aliasing.
- granular.density — was one occasional noise burst every 4th step. Now a REAL
grain cloud: a density-scaled scatter of short (12–40ms) pitch-varied grains
per step (0 at rest → a dense cloud when the world writes). noiseHit takes a
scheduled start time so grains scatter within the step.
The grimoire's existing claims ("sweeps the oscillator through its bank of
waveforms" / "a cloud of tiny grains") were aspirational — now they're accurate,
so no doc softening needed. Every clickable/routable thing now does what it says.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two voices native Web Audio can't do safely, done as one AudioWorklet module
(loaded from a Blob URL, so still single-file):
- 🎸 Karplus-Strong string — a sample-accurate 8-voice plucked-string worklet
(noise burst → damped delay loop). Right-click sky → "string voice" swaps the
FM tine for it. Verified offline: STABLE (rms 0.04, not the billions the native
version exploded to), in-tune (A3→221, A4→436), harmonic.
- 🔊 12-bit bitcrush — a world-drivable lo-fi stage on the master bus (true
bit-depth quantise + sample-rate reduction). Route anything to `crush`; clean
by default (16-bit → 4-bit + downsample). Verified: transparent at 16-bit,
hi-freq grit 0.18 at 4-bit.
Loaded async on synth start, spliced glitch→crusher→out and karplus→preSat.
Verified live: both load with no errors, string-voice toggle flips correctly.
Grimoire + manual document the whole synth-voice suite.
This closes the timbre work: FM tine · supersaw · chorus · string · bitcrush —
every one tuned by the offline-render + FFT harness before it made a sound.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The whole visual now zooms: scroll or trackpad-pinch to zoom the canvas toward
the cursor (1×–6×), drag empty space to pan when zoomed, double-click to snap
home. Implemented as a CSS transform on #stage (smooth, no draw-loop surgery);
all four hit-test chokepoints (mousedown/move/up/contextmenu) invert the zoom so
clicking/dragging/right-clicking nodes still lands exactly — provably correct
(the inverse of the transform). Pan clamps so the canvas always covers the view.
Grimoire + manual note the gesture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Supersaw: the pad's 3 chord tones each become a detuned saw cluster (9 saws,
±14¢ spread) — the lush JP-8000 wall. Verified offline: lush (133 partials),
no clip (peak 0.29), no aliasing, no DC; padAmp trimmed 0.10→0.07 for the extra
voices. Retuning loop + level updated for the new {o,iv} structure.
FM finish: bumped FM_RATIO 2→3 (the classic bright DX7 tine). Verified alias-free
across the whole register — even ratio 4 at A6/hard aliases <0.2% — so brightness
is pure taste, not a correctness risk. One-line const if you want warmer (2) or
glassier (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pluck() (used by the sequencer, hand notes, and mind mode) is now a sine carrier
phase-modulated by a sine operator (ratio 2), with the mod index enveloped —
a bright "tine" attack decaying to a mellow tail — and scaled by velocity, so
harder = brighter (the DX7 magic). PM not FM, for stable pitch.
Built via the new spectral-verification method: rendered offline in an
OfflineAudioContext and FFT-analysed BEFORE shipping. Verified: in-tune
(A4→439Hz, A2→110Hz), harder-is-brighter (centroid 570→858), zero aliasing,
zero DC, stable. The harness earned its keep immediately — it caught a
Karplus-Strong feedback explosion (rms in the billions) that would've shipped
blind; KS + bitcrush deferred to an AudioWorklet batch (native feedback + filters
is unstable under Web Audio block-processing).
Taste (the FM ratio/index) is the one thing left for real ears. FM_RATIO is a
one-line const to tune.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The grimoire got big, so give it a proper entrance:
- ⚡ Quick-start card up top: a 7-step "make it sing in a minute" on-ramp for the
generative sequencer (scale → euclid → Play One → ratchet → p-lock → play).
- ☰ contents: a floating, jump-anywhere table of contents built from the 22 h2
sections (delta-based scroll so it lands reliably in any layout). Sits beside
the × close, drops a clean index panel, closes on pick or outside-click.
Verified: quick-start renders (7 steps), TOC lists all 22 sections and jumps
distinctly (Getting in→top, Playing it→far). Manual regenerated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the auto-derived skin scale (which gave 3× C wholetone and a sparse
hirajoshi default) with a curated SKIN_HOME map — a considered {root,scale} per
cosmology, every pair unique so turning the wheel always changes the key.
Verified: 14/14 distinct. Default (chakra) is now warm C minor pentatonic.
chakra C min-pent · planes A lydian · tree D minor · lodge G major ·
ladder E phrygian · yggdrasil F# minor · lataif B dorian · wuxing F# pentatonic ·
tarot A minor · zodiac C mixolydian · futhark G dorian · wheelyear F major ·
artist C wholetone · bagua E min-pent. (Fallback to derived scale for any future skin.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Steal the one clever idea from the PO/Elektron world: p-locks. A CC/texture
voice (filter, glitch, reverb, drive) now gets a per-step LOCK lane — pin its
value to a blocky level on any step, overriding the world there; drag a step
below the lane to unlock it and hand it back to the data. The filter jumping to
a different cutoff on every hit, disjointed and alive — a full groovebox.
- applyGroove(sq, step, world) unifies the gate + p-lock; computeDests calls it.
- lock[] rides on seqs (serialised, migrated). Note voices keep velocity/chance/
ratchet; CC voices get the p-lock lane (amber).
- self-check guards all four cases (no-groove / lit-unlocked / dark / locked).
- grimoire + manual updated (also documents the Juno chorus).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>