📋 perihelion: wave-2 brief — four parallel lanes (NEARGOD·SELENE·SYZYGY·VESSEL) (fable)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
monsterrobotparty 2026-07-16 09:10:23 +10:00
parent 6391273ac5
commit e7bcf424d7

257
PERIHELION_BRIEF.md Normal file
View File

@ -0,0 +1,257 @@
# 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 `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 <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
```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=<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.*