# SOLARGOD — execution brief **For: Opus 4.8. Read this whole file before writing any code.** SOLARGOD is a standalone, browser-based 3D solar system explorer — the sibling of GODSIGH (the CesiumJS OSINT globe at `/Users/m3ultra/Documents/GODSIGH`, read-only reference; do not modify it). GODSIGH proved the house architecture: **no build step, ES modules, a layer-factory contract, a HUD with per-layer status lines, a scrubbable time slider with live/scrubbed gating, shareable hash-state URLs, and a zero-dependency `serve.py` that statically serves the app and proxies the feeds a browser can't call directly.** SOLARGOD transplants all of that onto **Three.js** — Cesium is an Earth-geodesy engine and is wrong for interplanetary space. The experience: you load the page and you're floating above a **compressed-scale** ("MEGA mode") solar system — every planet visible at once, moving on real ephemerides, with a time bar you can scrub across centuries. Click Jupiter and the camera flies out and settles into orbit around it, moons circling. Toggle TRUE scale and the system animates out into its real, humbling emptiness. Live layers show where Voyager 1, JWST and Parker Solar Probe are *right now*, real asteroids, comets, and close approaches. Everything client-side, free, keyless. --- ## 0. Ground rules - **No build step. No npm. No framework. No TypeScript.** Plain ES modules loaded by the browser, Three.js via CDN importmap (pin an exact version, e.g. `three@0.170.0` from jsdelivr; you need the core + `three/addons/` for OrbitControls, CSS2DRenderer, Lensflare). Vendoring the two Three.js files into `vendor/` instead of the CDN is acceptable and preferred if trivial, so the app works offline. - **serve.py**: stdlib-only Python static server + API proxy (see §6). Default port **8147** (GODSIGH owns 8137). Quickstart must be exactly `python3 serve.py` → open `http://127.0.0.1:8147`. - **Deterministic first**: the core scene (Sun, planets, moons, orbits, time travel) must work with **zero network calls** — planet positions are computed locally from Keplerian elements (§3), the way GODSIGH computes satellites from TLEs with SGP4. Network-dependent layers (spacecraft, asteroids, comets) are additive and must fail soft with a status line in the HUD, never break the scene. - **Truth before view**: every position is computed in real heliocentric J2000-ecliptic AU first, then passed through the view transform (§4). Never compute directly in view units; never mix the two spaces. One module owns the transform. - **No n-body integration.** The Standish Keplerian approximation IS the spec. Resist the urge to be more accurate than the mission needs. - **Verification gates are mandatory** (§9). Each stage ends with its gate passing; don't move on with a failing gate. Measurement beats reasoning. - Git: work on `stage/N-name` branches, merge to `main` when the stage gate passes, push to `origin` (Gitea, already configured). Commit style matches the house: emoji prefix + short message + `(opus)` suffix, e.g. `🪐 kepler: planets on rails — ephemeris engine + orbit paths (opus)`. ## 1. Repo layout ``` solargod/ index.html # importmap, canvas, HUD skeleton, time bar serve.py # static + Horizons/SBDB/CAD proxy + disk cache css/style.css # dark UI, HUD, labels, time bar js/ config.js # tunables: scale constants, camera, layer defaults, body registry refs lib.js # time (JD/centuries), math helpers, eclToWorld(), formatters ephem.js # Keplerian propagator + element tables (§3) bodies.js # physical data registry: radii, tilts, rotation, colors, texture paths scale.js # THE view transform: MEGA/TRUE compression, radius exaggeration (§4) ui.js # HUD rows, status lines, LIVE/SCRUBBED chip (port GODSIGH's ui.js idioms) main.js # bootstrap: renderer, clock, focus/camera system, layer loader, picking, hash state layers/ planets.js # Stage 1 moons.js # Stage 3 rings.js # Stage 3 spacecraft.js # Stage 4 asteroids.js # Stage 5 comets.js # Stage 5 neos.js # Stage 5 assets/textures/ # 2k textures + CREDITS.md (§7) cache/ # serve.py disk cache — gitignored verify.html # in-browser test runner (§9) SOLARGOD_BRIEF.md # this file ``` **Layer contract — identical to GODSIGH** (`js/layers/*.js` default-exports a factory): ```js export default function create(ctx) { const { scene, camera, CONFIG, lib, ui, scale, ephem, bodies, focus, clock } = ctx; // ui.addLayer(id, name, defaultOn, onToggle); ui.setStatus(id, text, 'ok'|'warn'|'err') return { id: 'x', onClockTick(simDate, jd, isLive) {}, // every frame; keep allocation-free handlePick(intersect) { return false; }, clearPick() {}, }; } ``` `main.js` loads layers with dynamic `import()` + `Promise.allSettled` so one broken layer never kills the app (copy GODSIGH `main.js:179-208` idiom). ## 2. Time system - Simulation clock lives in `main.js`: a float64 unix-ms `simTimeMs`, advanced each frame by `rate * dtWallMs`. `lib.js` provides `jdFromUnixMs(ms)` (JD = ms/86400000 + 2440587.5) and `centuriesSinceJ2000(jd)` (T = (jd − 2451545.0)/36525). Ignore TT/TDB−UTC (~69 s) — irrelevant at this scale; note it in a comment. - **Range: 1800–2050** (Table 1 validity). Clamp scrubbing to it. An extended-range mode (Table 2a/2b, 3000 BC–3000 AD) is a stretch goal — build the table plumbing so it's a config swap, but don't build UI for it in v1. - Controls (bottom bar): **NOW** button, play/pause, a rate control with presets `1×real · 1 min/s · 1 hr/s · 1 day/s · 1 wk/s · 1 mo/s · 1 yr/s` (also negatives for reverse), a scrubbable timeline strip spanning the full range with a zoomed inner strip around the cursor, and a date readout (UTC). - **LIVE / SCRUBBED chip** exactly like GODSIGH: live = |sim − wallclock| ≤ 60 s at 1× rate. Live-only network layers pause off-live where that matters (spacecraft layer interpolates its cached span instead). ## 3. Ephemeris engine (`ephem.js`) — Standish approximate elements Source: JPL SSD "Approximate Positions of the Planets" (https://ssd.jpl.nasa.gov/planets/approx_pos.html). The tables below were extracted from that page on 2026-07-15 — **they are authoritative, use them verbatim.** (The page's `e` column header says "rad" — that's the page's own typo; `e` is dimensionless. Angles are degrees, rates per Julian century.) **Table 1 — J2000 ecliptic elements + rates/century, valid 1800–2050 AD** (rows: `a[au] e I[deg] L[deg] long.peri ϖ[deg] long.node Ω[deg]`, second line = rates/Cy): ``` Mercury 0.38709927 0.20563593 7.00497902 252.25032350 77.45779628 48.33076593 0.00000037 0.00001906 -0.00594749 149472.67411175 0.16047689 -0.12534081 Venus 0.72333566 0.00677672 3.39467605 181.97909950 131.60246718 76.67984255 0.00000390 -0.00004107 -0.00078890 58517.81538729 0.00268329 -0.27769418 EM Bary 1.00000261 0.01671123 -0.00001531 100.46457166 102.93768193 0.0 0.00000562 -0.00004392 -0.01294668 35999.37244981 0.32327364 0.0 Mars 1.52371034 0.09339410 1.84969142 -4.55343205 -23.94362959 49.55953891 0.00001847 0.00007882 -0.00813131 19140.30268499 0.44441088 -0.29257343 Jupiter 5.20288700 0.04838624 1.30439695 34.39644051 14.72847983 100.47390909 -0.00011607 -0.00013253 -0.00183714 3034.74612775 0.21252668 0.20469106 Saturn 9.53667594 0.05386179 2.48599187 49.95424423 92.59887831 113.66242448 -0.00125060 -0.00050991 0.00193609 1222.49362201 -0.41897216 -0.28867794 Uranus 19.18916464 0.04725744 0.77263783 313.23810451 170.95427630 74.01692503 -0.00196176 -0.00004397 -0.00242939 428.48202785 0.40805281 0.04240589 Neptune 30.06992276 0.00859048 1.77004347 -55.12002969 44.96476227 131.78422574 0.00026291 0.00005105 0.00035372 218.45945325 -0.32241464 -0.00508664 ``` **Table 2a — same format, valid 3000 BC–3000 AD** (ship it in the file, unused by v1 UI): ``` Mercury 0.38709843 0.20563661 7.00559432 252.25166724 77.45771895 48.33961819 0.00000000 0.00002123 -0.00590158 149472.67486623 0.15940013 -0.12214182 Venus 0.72332102 0.00676399 3.39777545 181.97970850 131.76755713 76.67261496 -0.00000026 -0.00005107 0.00043494 58517.81560260 0.05679648 -0.27274174 EM Bary 1.00000018 0.01673163 -0.00054346 100.46691572 102.93005885 -5.11260389 -0.00000003 -0.00003661 -0.01337178 35999.37306329 0.31795260 -0.24123856 Mars 1.52371243 0.09336511 1.85181869 -4.56813164 -23.91744784 49.71320984 0.00000097 0.00009149 -0.00724757 19140.29934243 0.45223625 -0.26852431 Jupiter 5.20248019 0.04853590 1.29861416 34.33479152 14.27495244 100.29282654 -0.00002864 0.00018026 -0.00322699 3034.90371757 0.18199196 0.13024619 Saturn 9.54149883 0.05550825 2.49424102 50.07571329 92.86136063 113.63998702 -0.00003065 -0.00032044 0.00451969 1222.11494724 0.54179478 -0.25015002 Uranus 19.18797948 0.04685740 0.77298127 314.20276625 172.43404441 73.96250215 -0.00020455 -0.00001550 -0.00180155 428.49512595 0.09266985 0.05739699 Neptune 30.06952752 0.00895439 1.77005520 304.22289287 46.68158724 131.78635853 0.00006447 0.00000818 0.00022400 218.46515314 0.01009938 -0.00606302 ``` **Table 2b — extra terms added to M for Jupiter–Neptune, Table 2a only** (`M += b·T² + c·cos(f·T) + s·sin(f·T)`, everything in degrees, T in centuries): ``` Jupiter -0.00012452 0.06064060 -0.35635438 38.35125000 Saturn 0.00025899 -0.13434469 0.87320147 38.35125000 Uranus 0.00058331 -0.97731848 0.17689245 7.67025000 Neptune -0.00041348 0.68346318 -0.10162547 7.67025000 ``` **Algorithm** (per body, per tick — this is the whole engine, ~80 lines): 1. `T = centuriesSinceJ2000(jd)`; each element `x = x0 + ẋ·T`. 2. Argument of perihelion `ω = ϖ − Ω`; mean anomaly `M = L − ϖ` (+ Table 2b terms if using Table 2a for the outer planets). Normalize M to (−180°, +180°]. 3. Solve Kepler's equation `E − e·sin E = M` (radians) by Newton: `E₀ = M + e·sin M`, iterate `E -= (E − e·sinE − M)/(1 − e·cosE)`, tol 1e-9 rad, max 20 iterations. 4. Orbital-plane coordinates: `x′ = a(cos E − e)`, `y′ = a·√(1−e²)·sin E`. 5. Rotate to heliocentric J2000 ecliptic: ``` x = (cosω·cosΩ − sinω·sinΩ·cosI)·x′ + (−sinω·cosΩ − cosω·sinΩ·cosI)·y′ y = (cosω·sinΩ + sinω·cosΩ·cosI)·x′ + (−sinω·sinΩ + cosω·cosΩ·cosI)·y′ z = (sinω·sinI)·x′ + (cosω·sinI)·y′ ``` 6. **Earth = EM Bary** for rendering purposes (the barycenter offset is ~4 700 km, invisible at any scale we draw). The Moon is drawn by the moons layer around Earth. Expose `ephem.helioEcl(bodyId, jd) -> {x,y,z} [AU]` and `ephem.orbitPath(bodyId, jd, nSamples) -> Float64Array` (one full period sampled in mean anomaly, re-used for orbit lines; regenerate lazily when |T| drifts, elements change slowly). The same `keplerToEcl(elements, jd)` core must be reusable for asteroids/comets in Stage 5 (they come as epoch elements, not element rates: propagate with `M = M0 + n·(jd − epoch)`, n from `a` via Kepler's third law, `n[deg/day] = 0.9856076686 / a^1.5`). Skip hyperbolic/parabolic bodies (e ≥ 0.98) in v1 — filter them out with a status note. **Frame convention** (fix once in `lib.eclToWorld(v)`): ecliptic X → Three.js x, ecliptic Z (north) → Three.js y, ecliptic Y → Three.js −z (keeps the frame right-handed with Y-up). All of Three-space is "world"; nothing outside `lib.js` does this mapping by hand. ## 4. The view transform (`scale.js`) — "compressed mega" The whole product lives or dies here. Two scale modes, one smooth parameter. - **MEGA mode (default)**: compress heliocentric *radius only*, keep direction true: `r_view = K · r_AU^P` with **P = 0.4, K = 9.0** as starting tunables in `config.js` (puts Mercury ≈ 6.2, Earth 9, Jupiter ≈ 17.4, Neptune ≈ 35 scene units — the whole system fits in one ~80-unit view). Orbit *paths* must be built by compressing the sampled true positions with the same function so body and path always agree. - **TRUE mode**: `r_view = K · r_AU` (linear, 1 AU = K units). Mostly empty space. That's the point. - **The transition is a feature**: animate exponent `P` from 0.4 → 1.0 over ~2.5 s (smoothstep) when toggling. Everything — bodies, paths, trails — flows outward. Implement as `scale.radial(r_AU)` reading a live `P`; nothing else changes. - **Body draw radius**: true radius in AU (`r_km / 1.496e8`) × view K × an exaggeration factor `E`. Default E = 1200 for planets, Sun gets its own (E_sun ≈ 60, else it swallows Mercury). As the camera approaches a focused body, lerp its effective E → 1 based on `cameraDistance / (E · trueDrawRadius)` so you can descend to something like true scale in close orbit without being inside a balloon. Expose a body-scale slider (1× / 100× / 1200×) in the HUD. - **Moon-system compression** (Stage 3): moons get *local* compression around the parent: `d_view = R_parent_draw · (1.8 + 1.2 · log10(1 + d_true_km / R_parent_km))` — guarantees every moon clears the exaggerated parent surface and preserves ordering. In TRUE mode use plain linear true distance. Same animated blend. - **Floating origin (critical)**: float32 GPU precision jitters when the camera sits 1e-4 units from a position of magnitude 30+. Therefore: all true positions are kept in float64 JS; each frame, `focusPos` (the focused body's view-space position) is subtracted from every rendered object's position *in JS* before writing to the scene graph, and the camera orbits near the scene origin. Focus = Sun at boot. This also makes "in orbit around a moving planet" free — the planet stays at the origin and the sky moves. Use `logarithmicDepthBuffer: true` on the renderer for the huge near/far span. ## 5. Navigation & UX (`main.js` + `ui.js`) - **Focus system**: click a body (raycast, generous sphere hit targets) or pick from a body menu → animated camera flight (~2 s eased): tween the *focus point* from old to new body while tweening camera distance to `~6× the target's draw radius`, then hand over to OrbitControls targeting the origin (floating origin does the rest). Breadcrumb top-left: `SOL ▸ JUPITER ▸ EUROPA`. `Esc` steps out one level. - **Labels**: CSS2DRenderer DOM labels per body — name + (for the focused body's children) distance. Fade by screen density; never let labels overlap the HUD. - **HUD (right, GODSIGH style)**: layer rows with toggle + live status line (`spacecraft — 8 tracked, Horizons ok`), LIVE/SCRUBBED chip, MEGA/TRUE toggle, body-scale slider, mode credits footer. - **Hash state** (port GODSIGH `main.js:85-163` wholesale): `#f=&t=&s=&L=&cam=` — poll-serialize 1 Hz with `history.replaceState`, apply once at boot, reject malformed tokens (GODSIGH learned: `Number('')` is 0 — validate every token). - **Info panel** (Stage 6, left slide-in on focus): physical fact card (from `bodies.js`) + **live computed** facts — current distance from Sun and from Earth (AU + km), light-time ("sunlight is 43 min old at Jupiter right now"), current orbital speed, length of day/year. These live numbers are the soul of the tool — they come free from the ephemeris. ## 6. serve.py — static + proxy + cache stdlib only (`http.server`, `urllib.request`, `hashlib`, `json`, `pathlib`). Serves the repo dir; plus proxy endpoints (the browser cannot call JPL directly — verified: **no CORS headers on ssd.jpl.nasa.gov or ssd-api.jpl.nasa.gov**): - `/proxy/horizons?` → `https://ssd.jpl.nasa.gov/api/horizons.api?` - `/proxy/sbdb?` → `https://ssd-api.jpl.nasa.gov/sbdb_query.api?` - `/proxy/cad?` → `https://ssd-api.jpl.nasa.gov/cad.api?` Allowlist the upstream hosts + paths (never a general proxy). **Disk cache** in `cache/` keyed by sha256 of the full upstream URL: responses whose requested time span lies entirely in the past never expire; anything else TTL 24 h. Be polite to JPL — the cache is the rate limiter. Print one boot line like GODSIGH's serve.py. ## 7. Textures & assets - Planet/moon textures: **Solar System Scope texture pack** (CC BY 4.0, https://www.solarsystemscope.com/textures/) — download the **2k** maps for Sun, the 8 planets, Moon, plus the 8k→resized-to-4k Milky Way panorama for the skybox and the Saturn ring texture (PNG with alpha). Commit them under `assets/textures/` with an `assets/textures/CREDITS.md` (CC BY requires attribution; also credit in the HUD footer). Keep the committed set under ~25 MB total; downscale with `sips` if needed. - Every textured material needs a **procedural fallback** (flat color from `bodies.js`) so a missing file degrades gracefully. - Moons without textures: colored lambert spheres. Sun: emissive texture + additive sprite glow (+ subtle Lensflare addon if cheap). - Rings: Saturn — annulus geometry, inner 74 500 km / outer 140 220 km (true km, scaled with the parent's draw scale), ring texture, `DoubleSide`, transparent. Uranus gets a thin schematic gray ring. Tilt with the parent's axial tilt. ## 7b. Generated art — the MODELBEAST pipeline The M3 Ultra runs **MODELBEAST** (`/Users/m3ultra/Documents/MODELBEAST`, web UI `http://localhost:8777`, tailnet `http://100.89.131.57:8777`) — a local job queue/load manager with an on-device FLUX operator (`flux_local`, mflux/MLX, `flux2-klein-4b` ungated, seed-reproducible, **free and unlimited**). Drive it headless with the `mb` CLI (see `MODELBEAST/AGENTS.md` for the full reference): ```sh # shared agent credential — pre-provisioned, just source it (never commit/paste it): source ~/Documents/MODELBEAST/data/agent.env # sets MB_HOST + MB_TOKEN MB=~/Documents/MODELBEAST/mb # house workflow — run 5 seeds, pick the best: for S in 1 2 3 4 5; do $MB run flux_local -p prompt="$PROMPT" -p model=flux2-klein-4b \ -p seed=$S -p width=1024 -p height=1024 --wait --download art/candidates/ done ``` The credential is the `agents` guest user (verified working 2026-07-15: submit → generate ≈ 7 s/image → download). Guest = **local operators only** — flux_local, `bg_remove_local` (alpha cutouts), `seedvr2_upscale` — all free; the owner's paid fal/OpenRouter operators are blocked server-side, so generate without cost anxiety. The `gpu` lane runs one job at a time — queue all 5 and wait, don't spam retries on `queued`. If `data/agent.env` is missing, re-mint it with `cd ~/Documents/MODELBEAST && uv run python scripts/agent_token.py`; if MODELBEAST is down entirely, generate nothing and leave a TODO list of prompts in `assets/art/PROMPTS.md` instead — never block a stage on art. **The truth boundary (non-negotiable):** SOLARGOD is a scientific instrument. Real worlds get **real data textures** (§7 — NASA/Solar System Scope maps); FLUX must never invent the surface of Mars or the face of Europa. Generated art owns the *aesthetic* layer only, always visually distinct from data: - **Splash/boot screen** (Stage 0, optional polish): one hero image, brass-and-void observatory aesthetic to match the god-app family. - **Grimoire illustrations** (Stage 6): one stylized portrait per body for the info panel — engraving/astrolabe style, clearly *art*, framed separately from the rendered planet. ~800×800, 5-seed runs, pick best, note chosen seed+prompt in `assets/art/PROMPTS.md` so any card can be regenerated in-style. - **Effect sprites**: comet tail/coma, sun glow, spacecraft glyph icons — generate, cutout via `fal_bg_remove`, save as alpha PNGs. - **Optional "DREAM skybox"** toggle (off by default): a FLUX nebula panorama as an alternate sky. The *default* sky stays the real Milky Way panorama — from inside the solar system the real sky is a data statement too. FLUX doesn't produce seamless 2:1 equirects reliably; hide the seam behind the anti-camera side or mirror-wrap, and don't burn time here. Log every shipped asset in `assets/art/PROMPTS.md` (prompt, model, seed, operator chain) — reproducibility is the house rule for art too. The §7 texture budget (~25 MB committed) covers generated art as well; art candidates/rejects stay in `art/candidates/` (gitignore it). ## 8. Physical data (`bodies.js`) Registry keyed by body id with: `name, type, radiusKm, axialTiltDeg, rotationHours (negative = retrograde), color, texture, parent, elements|moonOrbit`. Radii to use: Sun 695 700 · Mercury 2 439.7 · Venus 6 051.8 · Earth 6 371.0 · Mars 3 389.5 · Jupiter 69 911 · Saturn 58 232 · Uranus 25 362 · Neptune 24 622 · Moon 1 737.4 · Pluto 1 188.3. Source the rest (tilts, rotation periods, moon data) from JPL SSD (https://ssd.jpl.nasa.gov/planets/phys_par.html and /sats/elem/) — **fetch and check, don't trust training memory for numbers.** Moons v1 roster (circular orbits in the parent's equatorial plane; correct `a` and period, phase = deterministic function of jd so scrubbing is stable). Starter values (verify against JPL): Moon 384 400 km / 27.322 d · Phobos 9 378 / 0.319 · Deimos 23 459 / 1.262 · Io 421 700 / 1.769 · Europa 671 034 / 3.551 · Ganymede 1 070 412 / 7.155 · Callisto 1 882 709 / 16.689 · Mimas 185 539 / 0.942 · Enceladus 238 042 / 1.370 · Tethys 294 672 / 1.888 · Dione 377 415 / 2.737 · Rhea 527 068 / 4.518 · Titan 1 221 870 / 15.945 · Iapetus 3 560 851 / 79.33 · Miranda 129 900 / 1.413 · Ariel 190 900 / 2.520 · Umbriel 266 000 / 4.144 · Titania 436 300 / 8.706 · Oberon 583 500 / 13.46 · **Triton 354 759 / −5.877 (retrograde)** · Charon 19 591 / 6.387. Pluto itself: treat as a Stage 5 small body (SBDB elements), but give it a real texture-less sphere and Charon. ## 9. Verification gates (`verify.html`) A standalone page that imports `lib.js` + `ephem.js`, runs assertions, renders a PASS/FAIL table. No test framework. **Ground truth from JPL Horizons** (heliocentric, J2000 ecliptic, AU, TDB — fetched 2026-07-15 from the live API; JD 2461236.5 = 2026-Jul-15 00:00 TDB): | Body | X | Y | Z | tolerance | |---------|---|---|---|-----------| | Mars (499) | +1.05351078 | +1.00862522 | −0.00469549 | ≤ 0.005 AU each axis | | Jupiter (599) | −3.01967256 | +4.33218144 | +0.04956505 | ≤ 0.01 AU | | EM Bary (3) | +0.38381080 | −0.94118486 | +0.00005421 | ≤ 0.003 AU | Add at least two more epochs (e.g. J2000.0 itself and 1900-01-01) by querying Horizons through the proxy from `verify.html` and comparing live — that also gates the proxy. Additional gates: Kepler solver converges for e = 0.97; `orbitPath` endpoints join; `eclToWorld` round-trips; MEGA→TRUE transition keeps direction (unit-vector dot ≈ 1). Spacecraft gate (Stage 4): Voyager 1 (`COMMAND='-31'`) on 2026-07-15 must plot at (−32.07, −136.21, +98.55) AU ecliptic — ~172 AU out, high above the plane; JWST (`-170`) must hug Earth (Δ < 0.02 AU). ## 10. Stages Branch per stage; gate before merge; one commit per coherent step. - **Stage 0 — GENESIS** (scaffold): repo layout, `.gitignore` (`cache/`, `.DS_Store`), serve.py static-only, index.html + importmap, renderer with logarithmic depth, Milky Way skybox, Sun with glow, OrbitControls, HUD + time-bar skeletons (static), 60 fps. Gate: boots clean, no console errors, `verify.html` scaffolding runs. - **Stage 1 — KEPLER** (the engine): `ephem.js` + tables, planets layer (textured, exaggerated), orbit paths, labels, full time system (§2) driving it, MEGA layout. Gate: §9 Horizons checkpoints pass; scrub 1800→2050 smoothly; retrograde loops of Mars visible when watched from Earth-focus (eyeball check). - **Stage 2 — HELM** (navigation): focus system, camera flights, floating origin, breadcrumb, MEGA/TRUE animated toggle, body-scale slider, hash-state URLs, proxy endpoints in serve.py (+ cache). Gate: jump Sun→Jupiter→back at 60 fps with zero visible jitter while clock runs at 1 mo/s; a pasted hash URL reproduces the view. - **Stage 3 — RETINUE** (worlds become places): moons layer w/ local compression, axial tilts, rotation (textures spin at true rates), sun-lit materials (day/night terminator falls out of a single point light at the Sun — verify Earth's night side faces away), Saturn/Uranus rings. Gate: Galilean moons orbit with correct relative periods (Io:Europa:Ganymede ≈ 1:2:4 — count laps at 1 day/s); Triton runs backwards. - **Stage 4 — PROBES** (the live layer): spacecraft via Horizons proxy. Roster: Voyager 1 `-31`, Voyager 2 `-32`, New Horizons `-98`, JWST `-170`, Parker Solar Probe `-96`, plus 2–3 more that resolve cleanly (check Juno, Lucy, Psyche ids via Horizons lookup API). Fetch VECTORS at 1-day steps spanning the clock window, through the cache; cubic-interpolate between samples; glowing marker + fading trail + label with live distance and one-way light time. Fail soft per craft. Gate: §9 spacecraft checkpoints. - **Stage 5 — SWARM** (small bodies): main-belt sample via SBDB query (~1–2 k asteroids with `a,e,i,om,w,ma,epoch`, propagated by the shared Kepler core, InstancedMesh points); NEO close approaches this ±30 d via CAD API (highlighted, clickable, miss distance in lunar distances); famous comets via SBDB lookup (1P/Halley, 2P/Encke, 67P, 96P — elliptical only) with full drawn orbits and anti-sunward tail sprite scaled by 1/r². All three are separate HUD layers, off-by-default except comets. Gate: belt sits between Mars and Jupiter (not a sphere, not a line); Halley's aphelion beyond Neptune's orbit… in the compressed view, near it. - **Stage 6 — ALMANAC** (knowing): info panel with live facts (§5), per-body prose blurbs (2–3 sentences, hand-written, grimoire voice), grimoire illustrations via MODELBEAST (§7b — skip gracefully if no token), Earth Easter egg — focusing Earth shows "▸ open GODSIGH world view" linking `http://127.0.0.1:8137`, README.md with the full GODSIGH-style layer table and screenshots (`docs/`), HUD credits. Gate: every clickable body has a card; README quickstart works from a fresh clone. - **Stage 7 — RESONANCE** *(optional, only if explicitly requested later)*: the godstrument bridge — serve.py WebSocket broadcasting `{body: {helioLonDeg, rAU, speedKms}}` at 10 Hz for sonification (music of the spheres: orbital periods as polyrhythms). Do not build in this pass. ## 11. Performance rules - Zero allocations in `onClockTick` paths — module-scope scratch `Vector3`s. - Orbit paths: build once, rebuild only when elements drift (|ΔT| > 0.1 Cy) or scale mode changes; the MEGA/TRUE transition may rebuild per-frame *during* the 2.5 s tween only (or better: write paths in true AU and compress in a tiny custom shader with `P` as a uniform — do this only if the CPU rebuild visibly hitches). - Asteroids: one InstancedMesh (or Points), positions updated at ≤ 10 Hz, not every frame, unless rate > 1 day/s. - Labels: update DOM at ≤ 5 Hz; cull labels behind the camera. - Target: 60 fps with all layers on, MacBook-class GPU; test with `stats`-style frame readout available behind `?debug=1`. ## 12. Known traps (read twice) - Float32 jitter — floating origin is not optional (§4). Symptom: focused planet "swims" at close zoom while time runs. - Mixing spaces — if a body and its orbit path disagree, someone compressed twice or not at all. One transform module, everything flows through it. - `M = L − ϖ` can exceed ±360° enormously for Mercury (L rate 149 473°/Cy) — normalize *after* the subtraction, in float64. - Horizons is text-first: with `format=json` the payload is still one big text blob in `result` — parse the `$$SOE…$$EOE` block by line. Request `VEC_TABLE='1'&REF_PLANE='ECLIPTIC'&CENTER='500@10'&OUT_UNITS='AU-D'`. - Don't hammer JPL while iterating — the disk cache exists so a dev reload costs zero upstream calls. Check the cache works before Stage 4 fetch loops. - Texture download links from solarsystemscope.com occasionally move — if a fetch 404s, fall back to procedural colors and leave a `warn` status, don't block. - Retrograde ≠ negative inclination — Triton is retrograde via i > 90° (or the negative-period convention in §8; pick one and comment it). - First Horizons call for a spacecraft can take 2–5 s — status line must say `fetching…` so it doesn't read as broken.