ADR-077 — /fly throne-of-glory iconic-shot architecture (body wiring + cislunar hold detector + per-event compositions + race-free test hooks)
Status · Accepted Date · 2026-06-14 Gates · v0.7.x ship; v0.8+ extension point for issue #341
Gating sentence: every flown / active mission across the four /fly segments (multi-flyby helio, single-flyby helio, Mars-direct, cislunar global) composes an iconic hero shot at its narrative beat via a per-body composition layer + a Tier-1.5 hybrid-waypoint hold detector + per-event MoonComposition variants; new destinations are added by following the 15-step §body-wiring checklist in TA.md.
Context
The pre-this-ADR state of /fly:
Iconic shots worked only for the 8 main planets + the Moon. New Horizons at Pluto / Arrokoth, Dawn at Vesta / Ceres, Giotto at Halley, Rosetta at 67P, OSIRIS-REx at Bennu — all fell back to "no body resolved" → cruise framing, or fell back to whatever planet
findClosestPlanetToShiphappened to think they were near (often Venus by default). Empty-space hero shots.Cislunar iconic shots worked for Apollo 11 + Chang'e 5, broke for everyone else. Free-return missions (Apollo 13 swing-by), direct landers (Luna 9 impact), powered-descent landers (Luna 16, Chandrayaan-3), low-energy transfers (SLIM), and the generic-
arrival-event missions (SLIM LOI, Chandrayaan-1 LOI) all sailed past the iconic moment with the camera at the wide Earth-Moon-system framing. The auto-zoom dispatcher only recognisedlunar_orbit/spiral_lunar/descent/ascentphases — and Tier-2 waypoint missions only ever got a singletli_coastphase out ofbuildFromWaypoints.Outer-system planets rendered dark. Sun PointLight had
distance=2000, decay=1.2— Neptune at 30 AU = 2400 scene units was beyond the cutoff and unlit.HUD showed
NaN km/spast the embedded Mars Hohmann SMA. Voyager 2 at Neptune (r=30 AU) againstaTransferAu = 1.262returned2/30 − 1/1.262 = −0.726→sqrt(negative)→ NaN.Same SPA mission swap broke under rapid URL pushes.
$page.url-tracking effect read$pagereactively insidelocaleFromPage(...), propagating noise from every unrelated$pagemutation.Verifying any of this was racing other Claude Code sessions using the same chrome-devtools-mcp browser instance. Pause clicks, scrubber events, and even page navigations were getting clobbered.
The throne-of-glory polish bar — the user-stated goal that every flyby compose at the same tier as Voyager 2's Neptune flyby photograph or Cassini's Saturn-OI hero shot — was a long way off. This ADR locks the architecture that closes that gap and makes it grindable per new destination.
Decision
1. Body wiring is a 15-step checklist with one canonical example
Adding a new flyby body (any destination — planet, dwarf, comet nucleus, asteroid, KBO, contact binary) is exactly 15 touchpoints across ~10 files. Arrokoth (commit e6e9175b) is the worked example end-to-end. The checklist lives in TA.md §body-wiring and AGENTS.md §"Adding a new flyby body to /fly", both pointing to the Arrokoth diff. Halley + 67P (commit 3c1e6938e) is a shorter follow-up adding the synonym pattern for ambiguous data labels ("Churyumov" → '67p').
The most subtle step is #7 labelToPlanetId + FLYBY_RADIUS_AU in src/lib/fly-mission-apply.ts. Without this, the trajectory.json waypoint at the body's label stays at the raw (x, y, z) coords from /explore (which can be 20+ AU off-axis from where destinationPos() says the body is), and the iconic composition frames empty space. Every new-body wiring failure I hit during this session traced back to a missing entry here.
2. Cislunar Tier-1.5 hybrid-waypoint hold detector
buildFromWaypoints walks every Tier-2 mission's cislunar_profile.waypoints_km for "hold" runs — 3+ consecutive entries with identical (x, y, z) within 10 km tolerance. Each hold is a data-designer signature for an iconic beat the analytic model couldn't compute (LOI capture, surface stay, TEI burn, free-return periselene). Multi-hold per mission is supported (Apollo 17 = LOI + surface stay + TEI).
Phase synthesis: tli_coast (outbound) → hold[0] (lunar_flyby or descent) → tli_coast (between holds) → hold[1] → … → tei_coast (after last hold). Phases interleave with no overlap; MET-ordered. descent if closest_approach_km ≤ R_MOON + 50 (Luna 9, Apollo 11 landing), lunar_flyby otherwise (Apollo 13 swing-by — the conservative LUNAR_PHASE_TYPES default; both compose iconically against the Moon).
Moon-local convention (the subtle bit): lunar_flyby and descent are in MOON_LOCAL_PHASE_TYPES. The sampler in $lib/orbital/sample-cislunar-spacecraft.ts does point + (live_moon − moonRefPos). The caller hard-codes moonRefPos = moonEciPos(flyby_day = dep + transit_days). So we store points = moonAtFlyby — then sample = moonAtFlyby + (live_moon − moonAtFlyby) = live_moon, pinning the spacecraft to the live Moon centre for the entire hold window. For Tier-1.5 hybrid data the hold's absolute coords don't align with the stylized moonEciPos model anyway, so storing the live-moon reference is the right convention. (Earlier attempts at storing (0, 0, 0) or moonAtSwingby had the spacecraft drift off-axis as simDay advanced.)
3. Per-event MoonComposition variants
Seven event types each drive their own MoonComposition:
| Type | Side | Pitch | camR× | targetBias | Use case |
|---|---|---|---|---|---|
loi | 85° | 15° | 4 | 0 | Apollo-8 earthrise default |
tei | 85° | 15° | 4 | 0 | (same) |
descent_start | 85° | 15° | 4 | 0 | (same) |
ascent | 85° | 15° | 4 | 0 | (same) |
flyby | 60° | 35° | 5 | 0.4 | Apollo 13 free-return limb arc |
edl_or_oi | 45° | 10° | 3 | 0.5 | Luna 9 / Chandrayaan-3 descent tightness |
arrival | 85° | 15° | 4 | 0 | SLIM / Chandrayaan-1 LOI (same as loi) |
findActiveCislunarHero widens to match all seven (was just loi/tei/descent_start/ascent). CISLUNAR_HERO_LEAD_DAYS carries per-event lead offsets. planCislunarHeroShot picks the right variant via CISLUNAR_HERO_COMPOSITION map; explicit ctx.composition override still wins for testing.
4. Live-moon hero tracking
planCislunarHeroShot's ctx.moonPos reads moonEciPos(simDay) — live moon, not Moon-at-iconicMet. The cislunarMoon mesh always renders at live position; previously the planner's cameraTarget = moonEciPos(iconicMet) drifted ~13°/day from the rendered Moon position, so users landing 0.1 d past iconicMet saw the moon out-of-frame. shipPosAtMet still uses iconicMet so the approach-direction geometry stays invariant inside the hero window.
5. Sun-lit hemisphere bias for flyby cinema
planFlybyShot flips the perp vector when cos(α) < −0.7 (α > 134° — camera deep on night side). At side-angle 85° the perp dominates the camera offset, so this controls which hemisphere ends up facing camera. V2 Jupiter pre-fix rendered as a thin-lit-limb at α ≈ 170°; post-fix is the iconic banded gas-giant lit hemisphere. Threshold -0.7 keeps the terminator-grazing compositions (V2 Uranus α ≈ 80°, Cassini SOI varies) — they're aesthetically valid Cassini-mission-art frames; we only flip the extreme back-lit cases.
6. MOI=arrival epilogue gate removed
For missions whose iconic moment IS the arrival event (Mars Express MOI = arrival, Mars Pathfinder EDL = arrival, etc.), the pre-fix gate epilogueActive && (rawActiveFlybyMet ≤ arrivalMet + 1) → suppress cinema immediately killed the iconic shot at landing. The gate's original purpose was Saturn-OI peakHold pinning. Gate removed; Saturn-OI still composes iconically (verified Cassini SOI). The peakHold release condition is a separate concern (open follow-up).
7. Race-free DEV-only test hooks
Two hooks added to src/routes/fly/+page.svelte:
window.__flySetSimDay(metDays)— setssimDay+ pauses. SkipsbiasJumpToIconicMoment/ 700 ms snap-cut / peakHold clearing — all upstream input concerns. Everything downstream (auto-zoom lerp, scene render, hero detector) runs identically to a real user landing at the same MET.window.__flyCislunarDebug()— mirror of__flyDebugfor cislunar missions (__flyDebugis gated by!isMoonMission). Returns the auto-zoom + camera + phase state needed to diagnose without spelunking Three.js refs.
Both DEV-gated. Used during this session to verify Apollo 13/17, Luna 9, SLIM, Chandrayaan-3 under contention with a podcast_scraper agent on the same Chrome instance.
8. Bonus items locked
- Outer-system lighting: Sun PointLight now
distance=0, decay=0(uniform sunlight). Neptune at 30 AU lit. Threshold for over-bright: at side-angle 85° + the flyby flip, all outer planets keep terminator visibility. - Vis-viva NaN guard:
heliocentricSpeed(r, a)returns circular-orbit speed (sqrt(µ/r)) whenradicand ≤ 0(r beyond transfer apohelion). At Neptune that's 5.43 km/s — coincidentally Neptune's mean orbital speed. - URL→mission effect isolated via untrack(): matches
feedback_svelte5_effect_untrackmemory; source-of-truth check short-circuits same-URL re-fires. - NH mission arc extended to Arrokoth (
arrival_date 2015-07-14 → 2019-01-01,transit_days 3463 → 4730). Pluto stays an iconic flyby event mid-arc; Arrokoth becomes arrival.
Consequences
Positive:
- Adding a new flyby destination is a grindable 15-step checklist with one canonical commit to copy from. Issue #341 (13 missing notable missions: Parker Solar Probe, OSIRIS-REx, DART, Lucy, Psyche, Europa Clipper, Mariner 9, Phoenix, Spirit, Opportunity, Magellan, Akatsuki, Hayabusa 1, Solar Orbiter) is well-scoped.
- Every existing mission in segments A-B-C-D composes at the throne-of-glory bar. Verified iconic this session: V1, V2 (Jupiter/Saturn/Uranus/Neptune), Cassini SOI, Galileo JOI, NH (Pluto + Arrokoth), JUICE JOI, Mariner 4, Juno, MESSENGER, BepiColombo, Dawn (Vesta + Ceres), Giotto/Halley, Curiosity, Perseverance, Tianwen-1, MAVEN, InSight, Hope, Mangalyaan, Schiaparelli, Mars Express, Apollo 11/13/17, Luna 9, SLIM, Chandrayaan-3, Chang'e 5, Vega 1, Venera 13, Ulysses.
- Multi-Claude-session contention on chrome-devtools-mcp is no longer a verification blocker.
- 340/340 orbital + lambert + cislunar + hero tests pass.
Negative / known limitations:
- Apollo 8/10/12/14/15/16, Change1, Luna 10, Smart-1, Lunar-prospector, Mariner 10, Apollo 7/9/1, Apollo-Soyuz, Starship-demo, Starship-mars-crew: missions ship without
flight.eventsarrays — data gap, not architecture. Filing follow-up in the next data-authoring session. - Apollo 17's authored waypoint data only has the post-TEI hold; LOI / descent / ascent rely on the live-moon track to compose correctly. Other Apollo missions may have the same single-hold limitation — fix is to author richer waypoints.
- Earth-orbit missions (
dest=EARTH: Apollo 7/9, Vostok, Mercury, Polaris Dawn, OTV) ride the cislunar branch with a Moon-bound trajectory model. The renderer composes but the trajectory shape is wrong for LEO/MEO/GEO. Separate-scene work; not blocking the throne-of-glory bar for the existing segments. - Saturn-OI peakHold suppression of the epilogue wide tableau survives. The cinema composes correctly at SOI, but the wide bookend pull-out after peakHold's release window is its own follow-up.
Related
- Commit
e6e9175be— Arrokoth as a first-class destination (canonical body-wiring example) - Commit
3c1e6938e— Halley + 67P comet wiring (synonym pattern) - Commit
2428b1d41— Pluto wiring (NH 2015 encounter) - Commit
034942a4e— Asteroid wiring (Vesta + Ceres + Psyche + Bennu) - Commit
f7919f654— Tier-1.5 hybrid waypoint hold detector - Commit
35902641b— Multi-hold + live-moon track - Commit
b98ce9c08— Sun-lit hemisphere bias for flyby cam - Commit
eb88418a2— Vis-viva NaN guard - Commit
88a6037ac— Outer-system lighting - ADR-058 — original cislunar scene + camera architecture
- ADR-030 —
/flymath + per-mission validation harness - GH issue #341 — 13 missing notable missions to add in a follow-up session