solargod/PERIHELION_BRIEF.md
monsterrobotparty e7bcf424d7 📋 perihelion: wave-2 brief — four parallel lanes (NEARGOD·SELENE·SYZYGY·VESSEL) (fable)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 09:10:23 +10:00

14 KiB
Raw Blame History

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 BC3000 AD) no CDN dependency; the full Table-2a range the engine already ships

0.2 Ground truth & hard gate

  • GATE: branch from main6391273. 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 <your port>/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-<lane> */) 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

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=<des> → 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 (~2040 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_<moon>.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 — SunEarthplanet 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·(PE)) / (|E||PE|) ) 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 20262028, 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 6080 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: 18002050 / 3000 BC3000 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 1011), 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.