310 lines
17 KiB
Markdown
310 lines
17 KiB
Markdown
# 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 <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
|
||
(~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/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.*
|
||
|
||
---
|
||
|
||
# Part ω — the closer (added post-merge, Fable 2026-07-16)
|
||
|
||
**Status: all four lanes MERGED to `main` (`b42473a`), 21/21 gates in-browser,
|
||
pushed.** N→M→E→V merged in order; integration fixes landed in review
|
||
(`sbdb_lookup` → `CONFIG.proxy.sbdbLookup`; era-aware BC dates in `lib.js`;
|
||
V's verify wait-guard raised 13→18 for the merged gate count).
|
||
|
||
### ω.0 Process note — orchestrator mode is now canon
|
||
|
||
Wave 2 ran as ONE Opus session dispatching four worktree-locked subagents that
|
||
built headlessly (node + serve.py/curl), with the orchestrator keeping the
|
||
single browser for all verification and real-input tests. **This is the
|
||
endorsed topology for all future multi-lane waves** — it preserves the
|
||
one-tree-one-writer rule and centralizes the thing there's only one of (the
|
||
browser). Lane briefs stay written for individual agents; the orchestrator
|
||
passes each its lane section verbatim.
|
||
|
||
### ω.1 The closer lane (one agent, branch `perihelion/o-closer` from ≥ b42473a)
|
||
|
||
1. **README refresh.** README.md still describes wave 1: CDN importmap (it's
|
||
vendored now), "schematic ring" NEOs (real orbits now), 13 gates (21 now),
|
||
no Epoch toggle / Sky events / moon textures / decade-century rates. Rewrite
|
||
the affected sections; keep the voice; update the layer table + controls +
|
||
verify table + credits (add USGS/NASA moon maps). New `docs/hero.jpg` via
|
||
the `/snap` endpoint if the old one no longer represents the app (deep-time
|
||
or Jupiter+textured-Galileans would sell it).
|
||
2. **Timeline event density** (E's flagged UX): the ±2 yr event window maps to
|
||
~1.6% of the 250-year strip (worse in deep time). Build the **event band**:
|
||
a second thin strip directly under `timeline-outer` that maps ONLY the
|
||
computed window to full width — diamonds live there (same DOM/click-jump
|
||
machinery, same CSS classes); the main strip gets a subtle bracket marking
|
||
the window's extent. Events layer owns it; no main.js changes beyond what
|
||
the layer contract already allows. Real-input gate: click a diamond in the
|
||
band → clock jumps; near-mode AND deep-mode.
|
||
3. **Housekeeping.** After user confirmation: `git worktree remove` the four
|
||
lane worktrees and delete the merged `perihelion/*` branches (their commits
|
||
live in main's merge history). Do NOT touch `../godstrument-*` worktrees.
|
||
4. **Full regression before report:** 21/21 (+ your band gate), zero console
|
||
errors in near and deep modes, README quickstart from a fresh clone.
|
||
|
||
### ω.2 Deferred (needs the user, not an agent)
|
||
|
||
- **HERALD** — deploy to the VPS (domain/tunnel decision → then mirror the
|
||
godstrument.pro rsync+systemd pattern; serve.py binds loopback, so prod
|
||
needs the same reverse-proxy shape godstrument uses).
|
||
- **Titan/Europa color** — M shipped grayscale where USGS has no color mosaic
|
||
(truth boundary held; ENDORSED in review). If the user wants iconic-color
|
||
moons, that's an explicit directorial choice to source non-data colorized
|
||
maps — flag it as such in CREDITS if taken.
|