ADR-026 — Multi-destination porkchop: data, semantics, and selectors
Status · Accepted Date · 2026-04-29 Closes · RFC-007 (Multi-destination porkchop) TA anchor · §components/lambert-worker · §components/plan-screen Related ADRs · ADR-008 (Lambert worker), ADR-016 (build-time assets), ADR-022 (Lambert protocol), ADR-023 (mobile magnifier)
Context
RFC-007 raised the question of extending /plan from a hardcoded Earth → Mars porkchop to "Earth → any planet" with optional landing/flyby semantics. The RFC proposed three interlocking sub-decisions: destination scope, mission-type semantics, and transfer-time range scaling. Five open questions sat under those: FLY-button experience for outer planets, build-time vs runtime grids, Y-axis units, C3-vs-tof axes, and whether Pluto/dwarf planets ship in v0.2.
This ADR locks all eight decisions in one document so the v0.1.6 implementation has a single contract to read.
Decision
Destination scope (RFC §what destinations to support — Option D-B)
Five destinations ship in v0.2 (= v0.1.6 milestone):
| id | Hohmann transit | tof_range_days | mission_types |
|---|---|---|---|
| mercury | ~110 d | [80, 250] | LANDING, FLYBY |
| venus | ~150 d | [80, 320] | LANDING, FLYBY |
| mars | ~250 d | [80, 520] | LANDING, FLYBY |
| jupiter | ~860 d | [400, 1500] | FLYBY only |
| saturn | ~2200 d | [800, 3000] | FLYBY only |
Mars defaults — no regression for the existing entry path.
Uranus, Neptune, Pluto, dwarf planets deferred to v0.3.0. Their multi-decade transfer windows + Lambert convergence concerns at extreme distances warrant the v0.2 lessons-learned before tackling them. Tracked under the v0.3.0 milestone.
Mission-type semantics (RFC §landing-vs-flyby — Option L-C)
Per-destination physical possibility drives what's offered:
- Inner planets (Mercury, Venus, Mars) — both LANDING and FLYBY enabled. The displayed cell ∆v on LANDING includes the per-destination
orbit_insertion_dv_km_sadded to the Lambert ∆v. - Gas giants (Jupiter, Saturn) — FLYBY only. The LANDING pill renders disabled with
aria-disabled="true"and a tooltip ("Landing on a gas giant is not a defined manoeuvre. See Galileo's orbit insertion in /missions for the closest analogue."). FLYBY ∆v = Lambert ∆v only.
Each per-destination JSON declares its mission_types array + a dv_orbit_insertion map keyed by the supported types.
Transfer-time ranges (RFC §transfer-time ranges — Option T-B)
Per-destination ranges declared per JSON file. Grid resolution stays 112 × 100 (preserves ADR-023 magnifier contract). Mars stays 80–520 d (no regression).
FLY-button experience (RFC OQ #1 — RESOLVED)
For non-Mars destinations, the FLY button navigates to /fly?mission={id}&dest={destination}&type={LANDING|FLYBY}. /fly renders an outbound-only arc using the v0.1.2 per-mission arc helper, with the destination planet substituted for Mars in the geometry. Free-return is reserved for the ORRERY DEMO scenario.
The HUD identity row reflects destination + mission type.
Build-time vs runtime grids (RFC OQ #2 — RESOLVED)
Build-time, per ADR-016. All five grids pre-computed by scripts/precompute-porkchops.ts (new) and committed under static/data/porkchop/earth-to-{id}.json. ~110 KB per destination → ~550 KB total, well within the existing image budget.
Trade-off accepted: instant first paint for any destination, full offline capability, no main-thread spike on slow devices when the user switches destinations. The Lambert worker still runs on main-thread interactions like a custom date-range slider (out of scope for v0.2 but the worker stays available).
Y-axis units (RFC OQ #3 — RESOLVED)
Auto-switch to years when tof_range_days[1] > 730. Mercury, Venus, Mars label the TOF axis in days; Jupiter and Saturn label in years (integer ticks: "1y", "2y", …). Threshold lives per-destination so tests can fix it.
C3-vs-tof axes (RFC OQ #4 — DROPPED)
Kept ∆v / dep-date axes. C3 is the mission-planner convention but adds an axis the user has to learn before reading the plot. PRD-002's promise is "intuitive before explanation"; ∆v stays the readable axis. C3 belongs to a future "advanced view" toggle if the educational case emerges.
URL contract
/plan?dest={id}&type={LANDING|FLYBY}destdefaults tomarsif absent (preserves existing /plan entry).typedefaults toLANDINGfor inner planets,FLYBYfor gas giants.- Invalid combinations (e.g.
?dest=jupiter&type=LANDING) coerce to the destination's first valid type and update the URL viareplaceState(no history pollution).
URL changes drive the porkchop swap; mission-type toggle writes back to URL via the existing pushFiltersToUrl pattern from /missions.
Data file contract
// static/data/porkchop/earth-to-{id}.json
{
"destination": "jupiter",
"dep_range_days": [0, 1460],
"tof_range_days": [400, 1500],
"steps": [112, 100],
"mission_types": ["FLYBY"],
"dv_orbit_insertion": {
"FLYBY": 0
// for inner planets, add: "LANDING": 5.5
},
"tof_axis_unit": "years", // "days" or "years"
"grid": [[ /* 112 × 100 of ∆v values in km/s */ ]],
"credit": "Computed at build time via Lambert solver. Ephemerides: planets.json."
}ajv schema: static/data/schemas/porkchop.schema.json. Validated on PR via the existing validate-data.ts step.
Lambert worker change
The worker accepts an optional destinationId parameter:
interface LambertRequest {
id: number;
depRange: [number, number];
arrRange: [number, number];
steps: [number, number];
destinationId?: 'mercury' | 'venus' | 'mars' | 'jupiter' | 'saturn'; // NEW
}When present, the worker reads the destination's heliocentric ephemerides (a, T, L0) from planets.json and parameterises the existing solver. Backward-compatible: if absent, defaults to 'mars'.
This generalises the worker from Earth → Mars only to Earth → any of the 5 supported destinations — closing the v2 issue noted in ADR-022's Consequences section.
Rationale
This ADR locks every variable RFC-007 raised in one place, so the v0.1.6 implementation can read a single contract. The decisions cluster around two principles already locked elsewhere:
- PRD-002: "intuitive before explanation" — drives the C3 drop and the years-not-decimal-decades axis convention.
- ADR-016: "external assets at build time" — drives the pre-computed grids decision.
Per-destination LANDING-vs-FLYBY semantics with the disabled-gas-giant pattern preserves the educational moment ("you cannot land on Jupiter") instead of hiding the question, matching PRD-002's "fail honestly" principle.
Alternatives considered
- All 8 planets in v0.2 (RFC Option D-A) — Uranus + Neptune transfer ranges (~12 + 31 years) make the educational pattern unreadable at 112×100 grid resolution. Defer is the safer call.
- Three destinations only (RFC Option D-C) — drops Mercury and Saturn, the two planets that produce the most striking educational outliers (Mercury: high ∆v despite being close; Saturn: 12-year wait between launch windows). Loses the load-bearing teaching moments.
- Toggle adds delta on client (RFC Option L-B) — orbit-insertion ∆v varies with arrival velocity, so a flat per-destination delta is imprecise. The two-grid approach (L-A) was equivalent, but L-C's "physical possibility" semantics is what the user actually expects (LANDING pill genuinely disabled on a gas giant > LANDING pill computing a misleading number).
- Auto-scaled ranges from Hohmann × constants (RFC Option T-C) — would miss interesting non-Hohmann transfer windows. Per-destination hand-tuning (T-B) is more work but produces tighter, more legible plots.
- Runtime computation per destination — re-does work on every visit, blocks first paint on slow devices, breaks ADR-016 offline-first principle.
Consequences
Positive:
/plancovers 5 of 8 planets, opening the educational story to inner-planet variation (Mercury's ∆v cliff, Venus's narrow windows) and outer-planet impossibility (Saturn's 12-year cycle).- Pre-computed grids: zero compute on the user's first load. Full offline capability preserved.
- Lambert worker becomes destination-parameterised — re-usable for v0.3 destinations + future custom porkchops.
- Mars regression-protected by the default-destination + range-preservation rules.
Negative:
- ~550 KB of pre-computed grid data committed to the repo (5 × ~110 KB). Acceptable per the existing 5.6 MB image bundle.
- New
scripts/precompute-porkchops.tsadds ~30 s to the build's first run (cached after that). Runs once locally + in CI; not a per-developer-visit cost. - Per-destination
orbit_insertion_dv_km_svalues must be sourced from public NASA technical reports per planet. Some hand-curation expected during v0.1.6 (tracked in milestone issues). - Three planets miss the v0.2 cut (Uranus, Neptune, Pluto) — explicitly tracked in v0.3.0 milestone with their own RFC frame.
Implementation notes
- New
scripts/precompute-porkchops.tsrunsnpm run precompute-porkchops(added tonpm run buildchain). - New
src/types/porkchop-grid.tsdefining the JSON shape. - New
static/data/schemas/porkchop.schema.jsonfor ajv validation. src/routes/plan/+page.sveltereads the active destination's grid file via$lib/data#getPorkchopGrid(destinationId).src/workers/lambert.worker.tsextendsLambertRequestwith the newdestinationIdfield.- Mobile magnifier (ADR-023) keeps working — grid stays 112×100 across destinations.
Issue tracking: v0.1.6 milestone (issues will be opened as a slice plan covering data + worker + UI + /fly + tests + docs).