Skip to content

ADR-039 — Bidirectional cross-link contract: site panels ↔ mission cards

Status · Accepted (closes RFC-012 OQ-4) Date · 2026-05-09 (back-filled from v0.4.0 implementation) Closes · RFC-012 OQ-4 ("/mars ↔ /missions cross-link UX") TA anchor · §components/cross-routing Related ADRs · ADR-013 (History API routing), ADR-024 (Mission URL sharing), ADR-037 (shared surface-site type)

Context

/mars (and /moon) site panels show landing-site detail; /missions cards show full mission detail. Many entries appear in both — Curiosity is a Mars surface site AND a mission. RFC-012 OQ-4 asked: how do the two views cross-link without drifting?

Three failure modes worth designing against:

  1. Drift — Curiosity's launch year is 2011 in /missions, but /mars accidentally says 2012 because two different overlay files describe the same thing. Schema should reject this.
  2. Lost context — user reads about Curiosity on /mars, clicks through to /missions, has no way back. URL contract should round-trip cleanly.
  3. Broken link/mars?site=curiosity opens a panel; that panel says "FULL MISSION CARD" linking to /missions?id=curiosity; the mission then says "ON THE SURFACE" linking back to /mars?site=curiosity. Both directions must work; either-direction rot is invisible without explicit tests.

Decision

Field name: mission_id

Every /mars and /moon site that corresponds to a /missions catalog entry carries a mission_id field referencing the mission's primary id. Validate-data enforces:

  • If mission_id is set, it MUST exist in static/data/missions/index.json (validate-data treats unknown ids as a fail-closed error per ADR-019).
  • Editorial fields that appear in BOTH the site panel and the mission panel (mission name, launch date, agency) SHOULD be sourced from the mission entry first, with the site overlay only adding surface-specific details (landing coordinates, surface time, samples returned, capability unlocked).

The site panel renders a chip / cross-link block titled "FULL MISSION CARD" (per UXS) when mission_id is set. Click navigates to /missions?id=<mission_id>. The mission panel opens pre-selected.

The mission panel renders a "ON THE SURFACE" chip when the corresponding mission id appears in either mars-sites.json or moon-sites.json. Click navigates to /mars?site=<id> or /moon?site=<id> (the resolver picks the body the site is on). The site panel opens pre-selected.

URL contract

Both directions are URL-state-driven:

/mars?site=curiosity
/moon?site=apollo11
/missions?id=curiosity

URL is the source of truth — back/forward in the browser navigates exactly through the cross-link history. Bookmarking either deep link reopens the right panel.

Drift guard

E2e test /mars round-trip per RFC-012 §Decision criteria #3:

/mars?site=curiosity → click "FULL MISSION CARD"
  → /missions?id=curiosity (mission panel open + named "Curiosity")
  → click "ON THE SURFACE"
  → /mars?site=curiosity (site panel open + named "Curiosity")

Same test exists for /moon round-trip with Apollo 11.

Rationale

  • mission_id field keeps the cross-link explicit + machine-checkable (validate-data can verify every link before ship).
  • URL state matches the existing routing contract (ADR-013 / ADR-024); no new mechanism needed.
  • One-way display rule ("editorial fields sourced from mission entry first") keeps the data flow obvious — mission JSON is canonical for shared fields; site overlay adds surface-only fields.
  • E2e round-trip test catches drift early — if either chip stops rendering or routes wrong, CI breaks.

Alternatives considered

  • Embed the mission record in the site overlay (one big merged JSON per site) — rejected because mission updates would have to fan out to two places. Schema-validated mission_id reference is cheaper.
  • Canonical mission JSON only, with site data as surface: {…} sub-object on missions — rejected because not every mission has a surface (orbiters, flybys); the surface-site catalogue would have nullable surface everywhere.
  • No automatic reverse link (forward-only) — rejected because mission users want to know "where on the surface did this land?" without guessing the site id.

Consequences

Positive:

  • One canonical place for shared fields (mission entry); site overlays add surface-only details.
  • URL contract round-trips cleanly; back/forward works.
  • E2e test catches cross-link rot.
  • Validate-data fails closed on unknown mission_id references.

Negative:

  • Site authors must remember to set mission_id for sites that have a corresponding mission — tracked by the e2e test (any new site without a mission_id for an existing mission would fail the round-trip if the cross-link is expected).

Implementation notes

  • src/types/surface-site.tsSurfaceSite.mission_id?: string (optional).
  • static/data/schemas/surface-site.schema.jsonmission_id as string, validated by scripts/validate-data.ts against missions/index.json ids.
  • src/lib/components/MissionCard.svelte (or equivalent on /missions MissionPanel) — renders "ON THE SURFACE" chip when site found.
  • src/routes/mars/+page.svelte, src/routes/moon/+page.svelte — render "FULL MISSION CARD" chip when mission_id set.
  • E2e: tests/e2e/mars.spec.ts covers Curiosity round-trip; tests/e2e/moon.spec.ts covers Apollo 11 round-trip.

Orrery — architecture documentation · MIT · No tracking