🌞 genesis: SOLARGOD brief — the solar system gets its instrument (fable)

Full execution spec for Opus 4.8: Three.js compressed-scale explorer on
GODSIGH's architecture DNA. Authoritative JPL Standish element tables,
Horizons ground-truth verification gates, 7 stages GENESIS→RESONANCE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-15 23:15:17 +10:00
commit cfdb0d6252
4 changed files with 467 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
cache/
.DS_Store
__pycache__/
*.pyc

19
CLAUDE.md Normal file
View File

@ -0,0 +1,19 @@
# SOLARGOD — Claude context
Standalone browser-based 3D solar system explorer (Three.js, no build step). The
complete execution spec is **SOLARGOD_BRIEF.md** — read it in full before coding;
it contains the authoritative JPL element tables, verification checkpoints with
Horizons ground truth, the stage plan, and the known traps.
- Sibling project & architecture reference: `/Users/m3ultra/Documents/GODSIGH`
(CesiumJS OSINT globe, port 8137) — **read-only**, never modify it. Port its
idioms (layer contract, HUD, hash state, serve.py proxy), not its code verbatim.
- Run: `python3 serve.py` → http://127.0.0.1:8147 (stdlib only, no deps).
- Origin: Gitea `ssh://git@100.71.119.27:222/monster/solargod.git`, default branch
`main`. Branch per stage (`stage/N-name`), merge on green gate, push. Commits:
emoji prefix + short message + `(opus)` suffix.
- Verification gates in `verify.html` are mandatory per stage — measurement beats
reasoning; never trust remembered constants over a fetched JPL value.
- `cache/` is serve.py's disk cache — gitignored, never commit it.
- Keep committed assets lean (~25 MB texture budget) and attribute CC BY sources
in `assets/textures/CREDITS.md` + the HUD footer.

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# SOLARGOD ▸ Solar System View
A browser-based **compressed-scale 3D solar system explorer** — real ephemerides,
scrubbable time across centuries, jump-to-orbit navigation, and live layers for
deep-space probes, asteroids, and comets. Sibling of
[GODSIGH](../GODSIGH/README.md), which watches Earth; SOLARGOD watches everything
else. Everything runs client-side from free, public, keyless data.
**Status: pre-build.** The full spec lives in [SOLARGOD_BRIEF.md](SOLARGOD_BRIEF.md).
## Quickstart (once built)
```sh
python3 serve.py
# open http://127.0.0.1:8147
```
## Planned layers
| Layer | Source | Notes |
|---|---|---|
| **Planets** | JPL Keplerian elements (Standish) | computed locally, no network — scrub 18002050 |
| **Moons & rings** | JPL SSD satellite elements | 21 major moons, Saturn/Uranus rings |
| **Spacecraft** | JPL Horizons | Voyagers, New Horizons, JWST, Parker — where they are *now* |
| **Asteroids** | JPL SBDB | real main-belt sample + NEO close approaches (CNEOS CAD) |
| **Comets** | JPL SBDB | Halley & friends, full orbits, 1/r² tails |
MEGA mode compresses distances so the whole system is one readable view;
TRUE mode animates it back out to its real, humbling emptiness.

415
SOLARGOD_BRIEF.md Normal file
View File

@ -0,0 +1,415 @@
# 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/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.
## 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), 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 25 s — status line must say
`fetching…` so it doesn't read as broken.