Skip to content

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).

jsonc
{
  // ... 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:

  1. Schema + data layer (slice 1.7a-1) — mission.schema.json extended; types updated; data.ts returns flight on getMission(); ajv validates; 3 reference missions populated (Curiosity, Apollo 11, Perseverance).
  2. /missions FLIGHT tab (1.7a-2) — new tab in MissionPanel alongside GALLERY and LEARN. Renders the populated fields, shows "—" for missing, banner for non-measured flight_data_quality.
  3. /fly HUD readout (1.7a-3) — when flight is present, the HUD shows real C3, V∞, total ∆v instead of (or alongside) the synthesised values. Spacecraft arc geometry uses real arrival.v_infinity_km_s if present.
  4. 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.
  5. Populate remaining 25 missions (1.7a-5) — each marked with appropriate flight_data_quality. Mariner 4 and Apollo missions reach measured; Mars 3 and Luna 9 land at sparse or reconstructed. Populated as research permits; PLANNED missions get flight_data_quality: "unknown" until they fly.
  6. 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=YYYY per 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=ID
  • from/to default 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=curiosity jumps the timeline to 2011 and highlights Curiosity (RFC OQ #4).

Schema validation rules

  • New flight sub-object: ajv validates structure when present, ignores when absent.
  • events[].type is 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 source so attribution is structural, not editorial. The honesty rule is baked into the schema.
  • PA §promises — "fail honestly": flight_data_quality lets 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_days would 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-level flight_data_quality flag 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:

  • /fly becomes mission-specific. Curiosity's arc is parameterised by Curiosity's C3 + V∞ + arrival geometry, not by a synthesised default.
  • /missions gains 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 sparse or reconstructed. 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_v lives until v0.2.x when we deprecate the string in favour of the structured flight.totals.total_dv_km_s. Tracked in IMPLEMENTATION.md.

Implementation notes

  • New static/data/schemas/mission.schema.json extension: add flight_data_quality (enum, optional), flight (object, optional with all sub-objects optional). No required-field changes.
  • New src/types/mission.ts extension: Mission.flight?: FlightParams with full sub-type tree. Each sub-object's fields all ?:.
  • src/lib/data.ts#getMission returns the new fields directly — no transformation. The merge with i18n overlay still adds events[].note from the overlay.
  • src/components/MissionPanel.svelte gains a FLIGHT tab (slice 1.7a-2). Uses the same tab pattern as GALLERY / LEARN.
  • src/routes/fly/+page.svelte reads mission.flight.totals.total_dv_km_s when present and falls back to parseDeltaV(mission.delta_v) when absent — preserves the v0.1.6 behaviour for missions yet to be populated.
  • New src/components/TimelineNavigator.svelte for 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's flight_data_quality.
  • Schema validation: per-event-type enum exhaustiveness checked in validate-data.ts so 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.

Orrery — architecture documentation · MIT · No tracking