solargod/SOLARGOD_BRIEF.md
monsterrobotparty 4b146bef68 🔑 brief: MODELBEAST agent credential pre-provisioned — source agent.env, no friction (fable)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 23:31:10 +10:00

29 KiB
Raw Permalink Blame History

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):

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/TDBUTC (~69 s) — irrelevant at this scale; note it in a comment.
  • Range: 18002050 (Table 1 validity). Clamp scrubbing to it. An extended-range mode (Table 2a/2b, 3000 BC3000 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 18002050 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 BC3000 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 JupiterNeptune, 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·√(1e²)·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=<bodyId>&t=<offsetSec|@unixMs>&s=<mega|true>&L=<on,ids>&cam=<dist,heading,pitch> — 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?<query>https://ssd.jpl.nasa.gov/api/horizons.api?<query>
  • /proxy/sbdb?<query>https://ssd-api.jpl.nasa.gov/sbdb_query.api?<query>
  • /proxy/cad?<query>https://ssd-api.jpl.nasa.gov/cad.api?<query>

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):

# 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 23 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 (~12 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 (23 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 Vector3s.
  • 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 25 s — status line must say fetching… so it doesn't read as broken.