# PERIHELION — SOLARGOD wave 2 · NEARGOD · SELENE · SYZYGY · VESSEL > **You are an execution agent (Opus).** You have been assigned ONE lane. Read > Part 0 fully, execute only your lane, end with the §0.6 report. The planning > model (Fable) reviews every lane before merge. Wave 1 (the full build) shipped > at `main@6391273` — 13/13 verify gates; you are polishing a working instrument, > not rescuing a broken one. **Do not break a passing gate.** --- ## Part 0 — shared context (all lanes read this) ### 0.1 The four lanes | lane | name | what | why | |---|---|---|---| | **N** | 🌑 NEARGOD | real orbits for the NEO close-approach layer | the one layer whose *positions* are schematic — make them true | | **M** | 🌖 SELENE | real textures + tidal lock for the major moons | worlds become places, twice over | | **E** | 🌓 SYZYGY | oppositions & conjunctions — computed sky events on the timeline | the almanac learns to predict | | **V** | 🛳 VESSEL | vendor Three.js (offline) + deep-time mode (3000 BC–3000 AD) | no CDN dependency; the full Table-2a range the engine already ships | ### 0.2 Ground truth & hard gate - **GATE:** branch from `main` ≥ `6391273`. Confirm: `git log --oneline -3` includes the 📡 NEO-status commit, and `verify.html` passes 13/13 in a browser **before you write a line** (`python3 serve.py ` → `/verify.html`). If either fails, STOP and report. - Read `SOLARGOD_BRIEF.md` (wave 1) for architecture: truth→view in `scale.js` only; floating origin; layer factory contract (`js/main.js` header comment); fail-soft network layers; §12 traps (all still apply, especially: don't hammer JPL — the serve.py disk cache is the rate limiter; Horizons/SBDB burst-throttle). - Multi-file repo (unlike godstrument's monolith) — territory is per-FILE here. Match house style: 2-space indent, `const`, terse precise comments, no deps. ### 0.3 Territory & merge order — what makes parallel work safe | lane | OWNS (may edit) | MUST NOT touch | |---|---|---| | N | `js/layers/neos.js` · `serve.py` (ONE `UPSTREAMS` line) · append-only `verify.html` gate | everything else | | M | `js/bodies.js` (texture/rotation fields only) · `js/layers/moons.js` · `assets/textures/` · `assets/textures/CREDITS.md` | ephem/scale/main; planet entries in bodies.js | | E | `js/layers/events.js` (new) · `js/layers/almanac.js` · append-only `css/style.css` block · append-only `verify.html` gate · ONE line in `js/main.js` `LAYER_MODULES` | everything else in main.js | | V | `index.html` · `verify.html` (importmap lines) · `vendor/` (new) · `js/config.js` (time block) · `js/main.js` | layers/, ephem.js math, scale.js | - **Merge order: N → M → E → V.** E and V both touch `main.js`; V merges last and rebases first (`git rebase main`) — V's owner expects this. `verify.html` and `style.css` additions are **append-only** (new rows/blocks at the end, marked `/* PERIHELION- */`) so they merge clean. - If you believe you must cross your boundary: STOP, log it as an open question in your report, work around it. ### 0.4 Git & run ```sh cd /Users/m3ultra/Documents/SOLARGOD git worktree add ../SOLARGOD-N -b perihelion/n-neargod # lane N git worktree add ../SOLARGOD-M -b perihelion/m-selene # lane M git worktree add ../SOLARGOD-E -b perihelion/e-syzygy # lane E git worktree add ../SOLARGOD-V -b perihelion/v-vessel # lane V ``` Work only in your worktree. **Dev ports** (serve.py takes the port as argv): N=8148 · M=8149 · E=8150 · V=8151. Port 8147 is the user's. Commit style: emoji + lowercase + `(opus)`, e.g. `🌑 neargod: the ring becomes real (opus)`. Never push to `main` — push your branch; Fable merges after review. ### 0.5 Hard rules (inherited from wave 1) - No npm, no build step, no frameworks. Stdlib-only serve.py. - Never commit `cache/` or `assets/art/candidates/`. - Verification gates are mandatory; measurement beats reasoning. Every lane adds at least one `verify.html` gate for its own work and re-runs the full suite (must stay green) before reporting. - UI input paths get a REAL-input test (the wave-1 lesson: OrbitControls was dead because everything was tested via JS — if you add anything clickable, click it via the browser tools, don't just call its handler). ### 0.6 The report (end your run with this) 1. What shipped (per feature, one line each). 2. Gate results with numbers (verify.html count + your lane's new gates + the full-suite re-run). 3. Any deviation from this brief and why. 4. Open questions for Fable. 5. Honest list of what you could not verify and why. --- # Lane N — 🌑 NEARGOD ("the ring becomes real") The close-approach layer currently draws REAL data (designation, miss distance, date, speed from JPL CAD) at SCHEMATIC positions — a ring around Earth, honestly labeled. Replace the schematic with truth. **N.1 The lookup.** CAD gives designations; orbital elements come from the SBDB *lookup* API (a different endpoint from the `sbdb_query` the belt uses). Add ONE line to serve.py's `UPSTREAMS`: `"sbdb_lookup": "https://ssd-api.jpl.nasa.gov/sbdb.api"`. Client-side: `proxy/sbdb_lookup?sstr=` → JSON with `orbit.epoch` (JD, TDB) and `orbit.elements` — an array of `{name, value}` STRINGS. Parse by NAME (`a, e, i, om, w, ma`), `+Number` each, epoch from `orbit.epoch`. Reject if any is non-finite or `e >= 0.98` (the wave-1 elliptical-only rule; note skips in the status line). Existing 24 h cache TTL covers this endpoint automatically — verify with a repeated fetch (`X-Solargod-Cache: hit`). **N.2 The layer.** Rewrite `neos.js`: for each CAD row (keep `dist-max=0.05`, `limit=40`), fetch elements **sequentially** through the proxy (never parallel — burst throttling), then propagate with the existing shared core (`ephem.smallBodyEcl`) exactly like the belt. Each NEO gets: marker sprite at its TRUE position each tick (reuse the reticle glyph; keep the miss-distance color ramp: <1 LD red, <5 amber, else green), its real orbit drawn on `worldGroup` via `ephem.orbitPathFromElements` (thin, 25% opacity, the marker's color), and the existing label/`handlePick` facts (des · LD · date · speed — all unchanged). Fail-soft PER OBJECT: a failed lookup falls back to the wave-1 schematic ring placement for that object alone, and the status line reports the split honestly: `38 real orbits · 2 schematic · CAD+SBDB`. First load is ≤40 sequential lookups (~20–40 s cold): status must count up `fetching 12/40…` like the spacecraft layer does. Cached reloads are instant. **N.3 Gate (append to verify.html).** Pick one NEO from the current CAD window programmatically; at its close-approach epoch (`cd`/`jd` from CAD), compute `|helio(NEO) − helio(EMB)|` from your propagated elements — it must be within **25%** of CAD's `dist` (generous: Earth≈EMB, two-body propagation, CAD dist is geocentric; the point is catching wrong-by-an-AU bugs, not chasing arcseconds). Full suite stays green. Real-input test: toggle the layer on in the browser and click one reticle. --- # Lane M — 🌖 SELENE ("the moons get faces") 21 moons render as flat-color spheres. Give the eight that matter real surfaces, and lock every moon's face to its parent. **M.1 Textures.** Targets: **Moon (has one already — leave it), Io, Europa, Ganymede, Callisto, Titan, Triton, Charon, Phobos** (skip any you can't source cleanly). Sources, in order of preference: USGS Astrogeology equirectangular mosaics, NASA/JPL photojournal maps, NASA 3D-resources — public-domain NASA/USGS imagery only (no fan maps, no unknown-license repacks). Downscale to **1024×512 JPG** (`sips -Z 1024 --setProperty format jpeg …`), target ≤150 KB each, **≤2 MB added total** (wave-1 committed size is 8.2 MB against a 25 MB budget). Files: `assets/textures/1k_.jpg`. Every file gets a line in `assets/textures/CREDITS.md`: source URL, mission/instrument, license. If a download 404s: skip, flat color stays (the wave-1 fallback is already in `moons.js` — verify it still works by feeding one bogus filename in dev, then remove the test). **M.2 Wiring.** `bodies.js`: add `texture:` to each sourced moon — touch NOTHING else in the file. `moons.js`: call the ctx-provided `loadTextureInto` (same as planets.js does) — the material/fallback machinery already exists. **M.3 Tidal lock.** Every major moon keeps one face to its parent. In `moons.js`'s tick: `mesh.rotation.y = θ + π` where θ is the SAME orbital angle main.js uses for the moon's position (`2π·(jd − J2000)/periodDays` — negative period gives retrograde rotation for free; copy the formula, don't import main.js internals). Eyeball gate: focus Jupiter at 1 hr/s and confirm Io's textured face toward Jupiter stays fixed across a full orbit. **M.4 Gate.** No verify.html math gate (this is a content lane) — instead: screenshot Jupiter-focus showing ≥2 textured Galileans, zero console errors, full suite still 13/13, texture bytes added reported exactly, CREDITS.md complete. Real-input test: click a textured moon → focus flight works. --- # Lane E — 🌓 SYZYGY ("the almanac learns to predict") Compute real sky events from the ephemeris — no network, pure math on `ephem.helioEcl`. **E.1 The finder** (`js/layers/events.js`, new — also export plain functions). Event types, all from heliocentric longitudes λ(body) = atan2(y, x) of the J2000-ecliptic position: - **Opposition** (outer planets, Mars→Neptune + Pluto): `normDegPM180(λp − λe)` crosses **0** — Sun–Earth–planet in a line, planet at its brightest. - **Solar conjunction** (outer): the same function crosses **180**. - **Inferior / superior conjunction** (Mercury, Venus): crossing **0 / 180**. - **Greatest elongation** (Mercury, Venus): local maxima of the GEOCENTRIC elongation angle `acos( (−E·(P−E)) / (|E||P−E|) )` where E, P are heliocentric vectors — scan for the derivative sign flip. Scanner: coarse 5-day steps across a window, detect sign changes / extrema, refine by bisection to <1 hour. API: `findEvents(jdStart, jdEnd) → [{jd, type, bodies, label}]` and `nextEvent(bodyId, jd)`. Compute lazily ±2 years around the current sim date, recompute when the clock leaves the computed window. Keep it off the hot path — run the scan in chunks (a few planets per frame or via `setTimeout(0)`), never a 4-year scan in one tick. **E.2 The timeline.** Events render as small diamond marks on `#timeline-outer` (DOM divs positioned by date fraction, your own append-only CSS block, brass for oppositions, dim for conjunctions). Hover title = label + exact date; click = jump the sim clock there (set `clock.simMs` via the ctx — the layer contract already hands you `clock`). HUD row: `Sky events` (default ON), status like `14 events ±2 yr · computed`. **E.3 The almanac hook.** In `almanac.js` (yours this wave): planets' cards gain one live fact — `Next opposition` (outer) / `Next elongation` (inner) with date and countdown, via `nextEvent()` imported from events.js. Keep the existing ≤5 Hz update discipline. **E.4 The one main.js line.** Add `'./layers/events.js'` to `LAYER_MODULES`. Nothing else in main.js. **E.5 Gates (append to verify.html).** (a) Mars opposition: known truth — **2027-02-19/20** (next Mars opposition). Your finder, scanned over 2026–2028, must produce a Mars opposition within **±2 days** of 2027-02-19. (b) Venus greatest elongations come ~72 days apart in alternating east/west pairs; assert two consecutive found elongations are 60–80 days apart. Full suite green. Real-input test: click a timeline diamond in the browser → clock jumps. --- # Lane V — 🛳 VESSEL ("cut the tether, open deep time") **V.1 Vendor Three.js.** Fetch the exact pinned files the importmap uses (`three@0.170.0`: `build/three.module.js`, `examples/jsm/controls/OrbitControls.js`, `examples/jsm/renderers/CSS2DRenderer.js` + anything they import — chase the import graph, it's shallow) into `vendor/three/`. Rewrite BOTH importmaps (`index.html`, `verify.html`) to relative vendor paths. Add a top-of-file comment in vendor/: version, source URL, MIT license note. Gate: **disconnect network** (or block jsdelivr via a hosts trick — simpler: rename the CDN domain in devtools offline mode) → full app boots, planets move, 13/13 gates minus the live-Horizons ones (which legitimately need JPL — note them as expected-fail offline, confirm they pass online). **V.2 Deep time.** The engine already ships Table 2a + 2b (`ephem.setExtendedRange`) — wave 1 built the plumbing, you build the door. HUD toggle `EPOCH: 1800–2050 / 3000 BC–3000 AD` next to MEGA/TRUE. Deep-time mode: `ephem.setExtendedRange(true)`, `CONFIG.time` swaps to `Date.UTC(-2999, 0, 1) … Date.UTC(2999, 11, 31)` (JS Date handles astronomical years fine), timeline end labels read from CONFIG (they're hardcoded `1800`/ `2050` in index.html today — fix that), and the hash gains `&e=1` so deep-time views share. **Toggle = reload**: orbit paths and element resolution are baked per-session by design, and you don't own the layers — so the toggle serializes state and reloads the page with `&e=1` in the hash (hash restore is already proven; add the `e` token to `applyHash`/`serializeHash`, which you own). This is deterministic, one line of behavior, and honest about what changes underneath. Spacecraft/NEO/ belt layers already fail-soft outside their data windows — verify they degrade gracefully at 1000 BC (spacecraft hidden, belt propagates fine, no errors). - **Time-rate presets**: deep time needs faster travel — append `1 decade/s` and `1 century/s` to `CONFIG.rates` (they're config-driven; the menu builds itself). **V.3 Gates (append to verify.html).** Deep-time accuracy: with `setExtendedRange(true)`, Jupiter heliocentric position at **JD 1355818.5 (1000 BC-01-01)** vs live Horizons through the proxy (`TLIST`, same pattern as gates 10–11), tol **0.15 AU** (Table 2a is an approximation over millennia — the gate catches wrong-table/wrong-term bugs, not arcseconds). Mercury at 2500-01-01, tol 0.02 AU. Confirm Table 2b's M-correction actually engages: with the correction terms zeroed in a scratch copy of the math (in-page, not in ephem.js), Jupiter@1000 BC must get WORSE — that proves the terms are live. Full suite green, online and offline-minus-live-gates. --- *Merge order N → M → E → V; Fable reviews each. HERALD (deploy to the VPS) is deliberately NOT in this wave — it needs the user's DNS/tunnel decision first.*