14 KiB
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 -3includes the 📡 NEO-status commit, andverify.htmlpasses 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 inscale.jsonly; floating origin; layer factory contract (js/main.jsheader 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.htmlandstyle.cssadditions 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/orassets/art/candidates/. - Verification gates are mandatory; measurement beats reasoning. Every lane adds
at least one
verify.htmlgate 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)
- 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
(~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_<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 — 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/sand1 century/stoCONFIG.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.