ADR-027 — Mission flight params + timeline navigator: data, semantics, and surfaces
Status · Accepted Date · 2026-04-29 Closes · RFC-009 (Mission flight params + timeline navigator) TA anchor · §components/mission-data · §components/fly-screen · §components/missions-screen Related ADRs · ADR-006 (mission JSON), ADR-017 (i18n overlay), ADR-019 (ajv validation), ADR-020 (canonical mission schema), ADR-024 (mission URL sharing), ADR-026 (multi-destination)
Context
RFC-009 frames the problem: the current mission JSON encodes identity (agency, dates, vehicle, payload) plus a single delta_v string. /fly synthesises trajectories from transit_days alone. /missions shows agency colour, year, and a one-line "first." None of this captures flight realism — the C3 a launcher delivered, the V∞ at arrival, the orbit-insertion burn that turned a flyby into an orbiter.
This ADR extends the mission record with a structured flight sub-object, adds an MET-stamped events[] array, locks the rendering rules for sparse / unknown data, and stages the rollout across /missions (FLIGHT tab), /fly (HUD readout), and a timeline navigator.
Decision
Schema extensions (RFC §what-flight-params — Option F-B, refined)
A new optional flight sub-object on every mission record. Every numeric field is itself optional — missing = unknown. The mission keeps its existing fields (no break in 28 mission files; the flight block is an addition).
{
// ... existing mission fields ...
"flight_data_quality": "measured",
// enum: "measured" | "reconstructed" | "sparse" | "unknown"
// top-level flag gates how the UI renders missing values; one per
// mission instead of per-field. Per RFC OQ #5 / Option S-C.
"flight": {
"launch": {
"vehicle_stage": "Centaur upper stage",
"c3_km2_s2": 11.2,
"declination_deg": 35.0,
"mass_at_tli_kg": 3893,
"source": "NASA MSL Press Kit, 2011-11"
},
"cruise": {
"tcm_count": 6,
"peak_heliocentric_speed_km_s": 33.4,
"source": "JPL MSL EDL Reconstruction, 2013"
},
"arrival": {
"v_infinity_km_s": 5.6,
"entry_velocity_km_s": 5.8,
"periapsis_km": null, // landers: null; orbiters: target periapsis
"inclination_deg": null,
"orbit_insertion_dv_km_s": null, // landers: null
"source": "NASA MSL Press Kit, 2011-11"
},
"totals": {
"total_dv_km_s": 6.1,
"tli_or_tmi_dv_km_s": 3.9,
"source": "computed from press-kit ∆v ledger"
},
"events": [
{ "met_days": 0, "type": "launch" },
{ "met_days": 0.005, "type": "tli_or_tmi" },
{ "met_days": 12, "type": "tcm" },
{ "met_days": 254, "type": "arrival" },
{ "met_days": 254.01, "type": "edl_or_oi" }
]
}
}Sub-objects (launch, cruise, arrival, descent, return, totals) are each optional. Each carries its own source string. Per-field optionality lets a mission report C3 and inclination but not declination if only those are public.
events[] is base-file (numeric/enum, language-neutral). Editorial notes for events live in the locale overlay under events[].note — already the convention from the existing event ticker (no new pattern).
Unknown handling (RFC §sparse-data — Option S-C): a top-level flight_data_quality flag plus per-field optional values. UI renders missing values as "—"; the FLIGHT tab and /fly HUD show a one-line caveat banner when flight_data_quality !== "measured".
Backward compatibility (RFC OQ #3)
The existing delta_v string field stays. New flight.totals.total_dv_km_s is the canonical numeric value /fly consumes. The pre-existing parseDeltaV() helper continues to handle missions whose flight.totals is absent. No mission file is broken by this ADR.
Surface rollout (RFC §where-to-surface — Option U-B, staged)
All three surfaces ship in v0.1.7, but in strict slice order:
- Schema + data layer (slice 1.7a-1) —
mission.schema.jsonextended; types updated;data.tsreturnsflightongetMission(); ajv validates; 3 reference missions populated (Curiosity, Apollo 11, Perseverance). /missionsFLIGHT tab (1.7a-2) — new tab in MissionPanel alongside GALLERY and LEARN. Renders the populated fields, shows "—" for missing, banner for non-measuredflight_data_quality./flyHUD readout (1.7a-3) — whenflightis present, the HUD shows real C3, V∞, total ∆v instead of (or alongside) the synthesised values. Spacecraft arc geometry uses realarrival.v_infinity_km_sif present.- Timeline navigator on
/missions(1.7a-4) — RFC Option N-C: horizontal dot strip on phones, decade-annotated horizontal axis on desktop. ~80 px tall above the filter bar; mission dots agency-coloured; drag-to-scrub a year range; pinch-zoom on mobile. - Populate remaining 25 missions (1.7a-5) — each marked with appropriate
flight_data_quality. Mariner 4 and Apollo missions reachmeasured; Mars 3 and Luna 9 land atsparseorreconstructed. Populated as research permits; PLANNED missions getflight_data_quality: "unknown"until they fly. - Tests + docs roll-up (1.7a-6) — unit tests for the new schema + render rules; e2e for FLIGHT tab + timeline navigator; v0.1.7 tag.
Timeline navigator (RFC §timeline-navigator — Option N-C)
[1957────1965────1975────1985────1995────2005────2015────2025────2030]
· · · · · · · · · · · · · ··· ·· · ·· · ·
[drag handle]──[drag handle]
[agency legend: NASA · ESA · CNSA · ROSCOSMOS · ISRO · JAXA · UAE]- Fixed range 1957 → 2030. Decade tick labels above the strip on desktop (≥768 px); abbreviated decade labels on mobile.
- Mission dots agency-coloured (existing
mission.color), 6 px on mobile, 8 px on desktop. Hover/tap → mission name tooltip. - Two drag handles bound a year range. Default = full range = no filter. Range syncs to URL via
?from=YYYY&to=YYYYper ADR-024. - Cards below filter to the windowed missions in real time, debounced at 100 ms (RFC OQ #5).
- Pinch-to-zoom on mobile expands the strip to a custom range (e.g., zoom into the 2020s decade); two-finger pan slides the window.
- Reduced-motion users get a year-range pair of
<input type="number">instead of the strip per ADR-025 tier-1.
URL contract (extends ADR-024)
/missions?from=YYYY&to=YYYY&dest=...&status=...&mission=IDfrom/todefault to the full 1957 → 2030 range when absent.- Both must be 4-digit years; out-of-range values clamp to the bounds.
- Card click writes
?mission=ID; deep-link to/missions?mission=curiosityjumps the timeline to 2011 and highlights Curiosity (RFC OQ #4).
Schema validation rules
- New
flightsub-object: ajv validates structure when present, ignores when absent. events[].typeis an enum:"launch" | "tli_or_tmi" | "tcm" | "arrival" | "edl_or_oi" | "flyby" | "earth_return" | "anomaly". Anything else fails validation; new types require a schema bump (additive only).- Numeric ranges are bounded but not pedantic: e.g.,
c3_km2_s2: { minimum: 0, maximum: 200 }, leaves room for outer-planet missions in v0.3.0.
Rationale
This ADR locks every variable RFC-009 raised in one place. The decisions cluster around three principles already locked elsewhere:
- PA §promises — "real mission data fully attributed": every numeric field carries a
sourceso attribution is structural, not editorial. The honesty rule is baked into the schema. - PA §promises — "fail honestly":
flight_data_qualitylets us distinguish "we don't know" from "this isn't worth filling" — no fake numbers, no silent gaps. - ADR-018 (mobile-first): the timeline navigator's mobile variant is the base; desktop adds decade labels and the legend.
Per-mission source strings (not a single top-level credit) means a mission can have measured launch params from a press kit AND reconstructed cruise data from a later JPL paper without conflating provenance.
Alternatives considered
- Option F-A (minimum viable) — too thin. Without arrival geometry the realism story doesn't reach the user (two missions with the same
transit_dayswould still look identical in/fly). - Option F-C (tiered: separate advanced JSON) — splits the data layer, doubles the cache surface, makes a contributor add a new mission across two files. Compactness in the base file isn't worth the architectural cost.
- Option S-B (verbose
{ value, source, confidence }per field) — doubles the JSON size and reads like a tax form. The top-levelflight_data_qualityflag carries the same information at one-tenth the verbosity. - Option U-A (defer timeline navigator) — ships flight realism but loses the "1957 → 2030 collapse felt visually" educational moment. The navigator is the narrative spine of the library, not just chrome.
- Option N-B (vertical timeline) — fights mobile-first. The horizontal strip reads at a glance, scales naturally to phones, and lets cards stay below in their existing grid layout.
Consequences
Positive:
/flybecomes mission-specific. Curiosity's arc is parameterised by Curiosity's C3 + V∞ + arrival geometry, not by a synthesised default./missionsgains a FLIGHT tab that surfaces 60 years of public flight data in one structured view per mission. Real numbers, real sources.- Timeline navigator turns the mission library from a card grid into a felt history — the launch-cadence collapse from "one per decade" to "one per year" reads visually.
- Schema is forward-compatible: optional fields + per-mission data-quality flag means we can add missions and fields without breaking existing files.
- Honesty rule is structural: a mission with sparse data shows that on its panel, not as fake numbers.
Negative:
- ~25 missions need flight-param research before their data reaches "measured." Some (USSR Mars probes; pre-1970 lunar) will permanently sit at
sparseorreconstructed. Tracked in v0.1.7 milestone. - Schema grows from 16 fields to ~16 + an optional sub-object tree. Validation step has more work to do; cost is sub-second.
- Timeline navigator adds a new interaction pattern that needs its own UXS spec + e2e coverage.
- Backward-compat shim for
delta_vlives until v0.2.x when we deprecate the string in favour of the structuredflight.totals.total_dv_km_s. Tracked in IMPLEMENTATION.md.
Implementation notes
- New
static/data/schemas/mission.schema.jsonextension: addflight_data_quality(enum, optional),flight(object, optional with all sub-objects optional). No required-field changes. - New
src/types/mission.tsextension:Mission.flight?: FlightParamswith full sub-type tree. Each sub-object's fields all?:. src/lib/data.ts#getMissionreturns the new fields directly — no transformation. The merge with i18n overlay still addsevents[].notefrom the overlay.src/components/MissionPanel.sveltegains a FLIGHT tab (slice 1.7a-2). Uses the same tab pattern as GALLERY / LEARN.src/routes/fly/+page.sveltereadsmission.flight.totals.total_dv_km_swhen present and falls back toparseDeltaV(mission.delta_v)when absent — preserves the v0.1.6 behaviour for missions yet to be populated.- New
src/components/TimelineNavigator.sveltefor the dot-strip + drag-handles UI (slice 1.7a-4). - New
src/lib/mission-flight.ts— pure helpers for "render a flight param value or—" given a mission'sflight_data_quality. - Schema validation: per-event-type enum exhaustiveness checked in
validate-data.tsso a typo in an event type fails CI. - Issue tracking: v0.1.7 milestone, slices 1.7a-1 → 1.7a-6 mirror the surface-rollout list above.