Skip to content

PRD-029 · Programs — a grouping layer for missions + fleet

Status · Draft (prep for v0.8) · 2026-07-11 Owner · Marko Builds on · PRD-004 (Mission Library) · PRD-012 (Spaceflight Fleet) · consumed first by PRD-025 (forward roadmap) May spawn · an ADR to lock the programs schema contract once the shape settles

Why this is a PRD. Programs add a whole new top-level surface (/programs) and a new editorial content type layered over the existing mission + fleet catalog — with its own voice, spine, roster model, badge pipeline, and per-entry dispatches. That's a product-shape decision touching navigation, the data model, the image/badge pipeline, and months of editorial authoring across every agency; it needs an agreed frame — what a "program" is, what it reuses vs. adds, and how it reads — before any route or program JSON lands. Hence a PRD, not a silent feature branch.

Problem

/missions (115) and /fleet (251) are flat lists. But almost every entry actually belongs to a campaign — Apollo, Voyager, Chang'e, Luna, Shuttle, ISS, Artemis, Project 921. The campaign is the unit humans actually think in ("the Apollo program", "China's crewed-lunar program"), and it's invisible in orrery today. Three consequences:

  1. No narrative spine. A learner sees Apollo 11 and Apollo 17 as unrelated cards; the arc — 6 landings, one fire, one near-loss, the hardware line — isn't surfaced.
  2. The campaign context is missing. A program can narrate its whole arc — Apollo's six landings, the fire, the near-loss — even mentioning missions orrery deliberately does not model. This is the honest full picture around our curated flagships.
  3. The forward roadmap needs it now. PRD-025 adds ~180 forward entries. Most arrive as programs (Artemis IV/V/VI + SLS + Orion + HLS; Project 921's ship + lander + suit + rover + rocket). Authoring the program first, then its members, is the natural — and less error-prone — order. This PRD defines that layer so PRD-025 is its first consumer.

Load-bearing principle — programs do NOT expand the curated set. Orrery's missions are a curated flagship set; we do not add a mission just because a program spans it. A program mentions its whole campaign (context) but cross-links only the flagship missions + fleet we already carry. Un-modeled missions are named, not modeled. "Programs reveal holes" means awareness (see the shape of a campaign vs what we chose to feature), never a mandate to backfill.

What a "program" is

A program is a named, multi-element space initiative with a shared goal, backing agency (or coalition), and typically dedicated hardware — grouping one-or-more missions and the fleet assets built for it.

But the grouping is the skeleton; the program's real substance is an editorial narrative. Think of it as the space-campaign analog of a PRD + an RFC fused into one: the PRD half is the why (the world it was born into, the motivation, the goals an agency was chasing), the RFC half is the what/how (the architecture, the hardware, the roster of missions), and the whole thing reads as a lightweight editorial piece — closer to a /science article than a data card. A program page should leave a curious reader understanding not just what flew but why it happened at all.

program.kind distinguishes the flavours (so campaigns and funding-lines coexist without forcing one shape):

kindexamples
crewed-campaignMercury, Gemini, Apollo, Vostok, Voskhod, Soyuz, Shenzhou, Artemis, Project 921 (China crewed lunar), Gaganyaan
robotic-campaignLuna, Zond, Venera, Mariner, Viking, Voyager, Pioneer, Surveyor, Ranger, Chang'e, Tianwen, Chandrayaan, Mars Exploration Program
stationSalyut, Skylab, Mir, ISS, Tiangong, ROS, BAS, Commercial LEO Destinations
infrastructureILRS, Moon-to-Mars / Lunar surface base, CLPS, Gateway (cancelled), Commercial Crew, Commercial Cargo, Deep Space Network
funding-line (optional)Discovery, New Frontiers, Flagship, Explorer, Great Observatories

Boundary rules (what is not a program): a single launcher's manifest (Falcon 9 flights) is not a program; a one-off probe with no siblings (e.g. a standalone comsat) has no program and that's fine — program is nullable. When a mission plausibly fits two (CLPS delivery for Artemis), pick the primary and cross-link the other. Nesting (Artemis ⊂ Moon-to-Mars) is out of scope for v1 — flat, single-primary; revisit if it earns its keep.

Editorial model + voice

Every program page is a short editorial piece with a fixed spine (each section is en-US prose first; i18n after the form locks):

  1. The land — where the world and the technology stood when the program began. Say the real reason out loud: Cold War prestige, superpower domination, a scientific frontier opening, a commercial bet, a nation's first reach for space. This is the why it happened at all.
  2. Goals — what the agency (or coalition) actually set out to do: the specific objectives and the outcomes they were reaching for.
  3. Outcome — what came of it: goals met, missed, cancelled, redirected, still open. State it; don't grade it.
  4. Narrative — the story that ties the roster together (the arc, the turning points, the human + engineering stakes). Lightweight editorial, not a textbook.
  5. Legacy — what it gave back. What the program did for humanity and for specific nations — economy, jobs, technology spinoffs, scientific return, soft power, inspiration, the international order it shaped. As the new space age opens, orrery advocates here for why this work matters — still neutral and sourced, but unafraid to name the material payoff (and the costs). Every program earns this section.
  6. What we can learn. The leadership / team-playbook / human-inspiration takeaway — a transferable lesson on doing hard things, grounded in the program, not preachy. A good story or a piece of trivia is fine, but there must be an element of "how to be better." (Apollo: a specific, dated, non-negotiable goal that aligned 400,000 strangers; Mission Control's "work the problem" teamwork under fire; doing it in eight years with computers weaker than a doorbell.) Every program pulls one.
  7. Roster — a timeline with a master-detail pane. Missions + hardware rendered time-oriented: a chronological timeline (year dots, linked flagships + named context). Selecting a mission loads its summary in a detail pane to the right of the timeline (hero, meta, blurb, "open full mission →") — reusing the whitespace instead of navigating away or stacking below. Per the roster model below.

Voice — non-negotiable: neutral, no fanboying anyone. Orrery is not a cheerleader for NASA, Roscosmos, CNSA, ESA, or anyone else. Name motivations honestly (yes, Apollo was Cold War domination and science; yes, the Soviet program had firsts the West downplayed) without triumphalism or dunking. We report intent and outcome; we are not the judge of whether a program "succeeded." Same global-representation bar as the rest of orrery (project memory: celebrate every agency, default to none). When sourcing is thin or contested, say so rather than pick a side.

Images — embedded, not decorative. A program page is a magazine piece: images live inside the editorial, between prose beats (the era's context, a launch, the crew, the hardware), each with a caption + credit — not just one hero. Two sources, reuse-first:

  • Reuse existing mission/fleet gallery images by reference — they already carry provenance (Apollo 11's own frames, a Saturn V shot).
  • Resource the context images the app doesn't have (Sputnik, Kennedy's Rice speech, a mission-control room) via the curated fetch pipeline (PRD-018): sourced, provenanced (CC/PD, no watermark), and approved per-image at /dev/staging like all image work. No web-search for URLs (project memory) — use the fetch scripts.

Content model: each editorial section's body is an ordered block list — { type: "prose", md } and { type: "figure", image, caption, credit }, where image is either { reuse: "missions/apollo-11/03" } (existing, provenance inherited) or a new { id: "the-land/sputnik" } under static/images/programs/{id}/.

Provenance & honesty surfaces (mandatory — release gate, not optional). Programs plug into the same bill-of-materials the rest of orrery honours:

  • Every new image added under static/images/programs/ is walked by build-image-provenance.ts into image-provenance.json (source, author, license, modifications) and shows up on /credits with a license summary — the credits.spec.ts ADR-047 gate fails the build otherwise (this is the exact test that red-lit v0.7.3). Reused images inherit their existing provenance.
  • Every source cited in a program (the links[] + any editorial citation) is documented on /library, the sources/outbound-link surface — same as missions/fleet learn-links.
  • No image ships without provenance + per-image /dev/staging approval; no source ships undocumented. Authoring a program includes its provenance + credits + library entries, not just its prose.

Image treatment (from the Apollo exemplar review). The hero opens the page and is never repeated lower down. Prefer more, smaller, single-subject images spread through the editorial over a few big full-bleed ones — and no collage / composite frames; pick clean individual photos (align: left/right/small, not always full).

Badges — a new artifact type to define + source. Mission patches and program insignia are iconic and currently absent from orrery. Model a badge (a program insignia + per-mission + per-fleet patch) sourced through the image pipeline like any image, and surface it for liveliness — on the program hero, on each roster mission/timeline node, and on the index card. Source badges per mission + per fleet asset as we work through each program (not a separate pass); Marko closes the gaps at the end once the corpus exists.

Internal cross-links — send readers deeper into orrery, not just out. A program links to our own surfaces:

  • the relevant body route/moon for a lunar program (both the landing-site level, deep-linking the Apollo sites on the Moon map, and the science/surface level), /mars for Mars programs, /explore for other bodies;
  • program-specific /science articles — the most notable (Apollo → free-return, trans-lunar injection, EDL, Tsiolkovsky / Saturn propulsion), and write new articles where a program needs one that doesn't exist yet.

Data model — where it lives

Follows the existing symmetric-cross-ref idiom (missions.fleet_refs[]fleet.linked_missions[], validated bidirectionally at build). One authored side, one derived side, fail-closed.

New first-class entity (parallel to missions + fleet):

static/data/programs/{id}.json           # program record
static/data/programs/index.json          # lightweight index (card grid)
static/data/schemas/program.schema.json  # + program-index + program-overlay schemas
i18n-src/{locale}/programs/{id}.json      # 14-locale overlays (name, summary, outcome)
static/images/programs/{id}/             # hero + gallery (deferred, like missions/fleet)

Following the mission/fleet split: the base record holds structured/non-translatable data; the en-US overlay holds the editorial prose (translated later, once the form locks).

Base record static/data/programs/apollo.json (sketch):

json
{
  "id": "apollo",
  "kind": "crewed-campaign",
  "agency": "NASA",
  "agencies": ["NASA"],
  "country": "USA",
  "start_year": 1961,
  "end_year": 1972,
  "status": "COMPLETED",           // PLANNED | ACTIVE | COMPLETED | CANCELLED
  "epoch": "space-race",
  "roster": [
    { "linked_id": "apollo-11", "kind": "mission" },
    { "name": "Apollo 12", "year": 1969 },          // context only — not modeled
    { "linked_id": "saturn-v", "kind": "fleet" }
  ],
  "links": [ { "l": "…", "u": "…", "t": "intro" } ]
  // member_missions[] + member_fleet[] DERIVED from mission.program / fleet.programs
}

en-US overlay i18n-src/en-US/programs/apollo.json (the editorial spine):

json
{
  "name": "Apollo",
  "best_known_for": "First crewed Moon landings — six between 1969 and 1972.",
  "the_land": "…Cold War, Sputnik shock, Kennedy's challenge — why it happened…",
  "goals": "…beat the USSR to the Moon; national prestige + lunar science…",
  "outcome": "…12 walked on the Moon; cancelled after Apollo 17…",
  "narrative": "…the arc, tying the roster together (lightweight editorial)…"
}

Membership (authored on the entries, derived on the program):

  • Mission gets program: "<id>" — single primary, nullable (standalone missions omit it).
  • Fleet gets programs: ["<id>", …] — array, because assets serve many (Saturn V → Apollo + Skylab; Falcon 9 → Commercial Crew + Commercial Cargo + CLPS + …).
  • A migrate-program-membership.ts script derives member_missions[] + member_fleet[] onto each program record (exactly like migrate-fleet-linked-missions.ts), and validate-data checks bidirectional integrity fail-closed: every program id resolves; every derived member points back.

Campaign roster (the full-arc picture without expanding the set). A program carries a roster[] describing the campaign's notable missions, each either linked ({ linked_id: "apollo-11" } → clickable to our flagship) or context-only ({ name: "Apollo 12", year: 1969 } → named, not modeled). The linked entries are exactly the flagships already in /missions; the rest give honest context. Whether roster is mandatory, how much is prose vs structured, and the fleet equivalent are open questions Slice 1 settles by doing the retrofit — don't over-freeze it here.

Alternatives considered:

  • Tag-only (no entity) — a program string + a lookup table. Rejected: no home for program metadata, no /programs route, no per-program i18n/images.
  • Program owns the member list (authored on program) — rejected: duplicates the mission/fleet authoring flow and drifts; the entry-authored + derived direction matches the repo's existing fail-closed pattern.
  • Many programs per mission — rejected for v1: one primary keeps the timeline unambiguous; secondary association is a cross-link, not membership.

Where it lives in the app

Settled 2026-07-11:

  • Top-level /programs (not a /science sub-tab) — programs span missions + fleet and are too big to bury. Modeled on /science's full-page editorial treatment, not the grid-plus-side-panel of /missions//fleet. Opening a program takes the whole page — never a side panel.
  • /programs index — editorial, not a dense grid. Era-spined by default (Space Race → Shuttle & Stations → ISS era → New Space & return-to-Moon → Forward), with a toggle to group by agency instead (both groupings supported). Rich, larger cards with a one-line hook under group headings — the browse itself reads as the arc of spaceflight.
  • /programs/{id} — full-page editorial: hero + status/era/agency, then The land → Goals → Outcome → Narrative with embedded images, then the roster as a chronological mission timeline + fleet hardware gallery (linked flagships clickable; context missions flat), then related programs + sourced links. Reuses the /science article shell.
  • Cross-links (deep-link, both ways): mission + fleet panels get a Program · Apollo ↗ chip → /programs/apollo; the program roster deep-links flagships to /missions?id= / /fleet?id= (their existing panels — reuses everything, no new detail surface).
  • Nav: /programs placed right after Fleet (Missions → Fleet → Programs → Plan → …) — the entities, then the stories that connect them.

Retrofit — the whole existing set (this IS Slice 1)

Not a cherry-picked pilot: Slice 1 invents the programs concept and retrofits everything orrery already carries, so the form is proven against real data at full breadth.

  1. Author program records for every campaign present in the current data — stations (ISS, Mir, Tiangong, Skylab, Salyut), crewed campaigns (Mercury, Gemini, Apollo, Vostok, Voskhod, Soyuz, Shenzhou), robotic campaigns (Luna, Zond, Venera, Voyager, Pioneer, Mariner, Viking, Chang'e, Tianwen, Chandrayaan, the Mars-robotics line), and the forward campaigns we already carry (Artemis — links artemis3/4; plus the other existing PLANNED entries: MMX, Blue Moon Mk1, Starship, Gaganyaan). "Get a glimpse" of the forward programs here.
  2. Tag all 115 missions + 251 fleet with program / programs[], populate each program's roster (link our flagships, name the rest as context).
  3. English-only. No 14-locale pass yet — the schema form isn't settled, so translating it would be premature rework. i18n lands after the form locks.
  4. The report-program-gaps.ts (per-program membership vs roster) is awareness only — surfaces campaign shape vs what we feature, not a backfill mandate (curated set is not expanded).

Integration with PRD-025 (forward roadmap)

The forward slices author the program first, then its members:

  • Slice 1 (NASA) → artemis + moon-to-mars + commercial-crew/clps programs, then tag Artemis III/IV/V + SLS/Orion/HLS/Moon-Base.
  • Slice 2-3 (China) → project-921 + chang-e + ilrs + tianwen + tiangong.
  • Slice 4 (Russia) → ros + luna + orel.
  • …etc. Each forward slice ships its program record(s) alongside its entries.

Slice plan

Slice 1 (this PRD) — invent + retrofit-everything, English-only. Executed in three steps so the form is locked before the bulk pass, without cherry-picking the scope (we still do all of it):

1a — Foundation: draft program.schema.json (+ index; overlay deferred), programs/index.json,
     /programs + /programs/{id} routes (reuse entity-card-grid), program/programs fields on
     mission + fleet schemas, migrate-program-membership.ts, validate-data wiring.
1b — Lock the form on a first exemplar end-to-end, Marko reviews → freeze mandatory vs optional.
     Exemplar order: Apollo (roster w/ linked+context, neutral Cold-War voice) → Mercury
     (the "Right Stuff" original US crewed program — the Mercury Seven) → ISS (multinational,
     long-running, fleet-heavy — stresses the collaboration case) → breadth from there.
1c — Bulk retrofit: author program records for every campaign in current data (incl. the
     forward ones we already carry — Artemis/MMX/…), tag all 115 missions + 251 fleet,
     populate rosters. English-only. Run the awareness gap report.

Then PRD-025 forward expansion (Slices 1–10 of PRD-025) authors each new program + its members under the now-frozen form. The 14-locale i18n pass for programs resumes once the form is locked (end of this slice / start of the forward work).

Decisions

Settled (2026-07-11):

  • Curated set is not expanded — programs mention the full campaign, link only existing flagships.
  • Retrofit depth = everything we already carry (all 115 missions + 251 fleet, incl. existing forward entries), as Slice 1.
  • English-first — no 14-locale pass until the form is locked.

Resolve during Slice 1b (by building the exemplar, then Marko reviews):

  1. Route — standalone /programs (recommended) vs a grouping toggle on /missions.
  2. funding-line kind — model Discovery / New Frontiers / Great Observatories as programs, or campaigns + stations + infrastructure only?
  3. Membership direction — entry-authored (program on missions, programs[] on fleet) + derived member lists (recommended, matches repo idiom).
  4. roster shape — mandatory or optional? how much prose vs structured linked/context entries?
  5. Nesting — flat/single-primary for v1 (recommended; revisit later).

Risks

  • Granularity bikeshedding — "is X a program?" The kind enum + boundary rules above are the guardrail; when unsure, default to no program (nullable) rather than inventing one.
  • Retrofit churn — tagging 366 existing entries touches a lot of files; do it as one bounded pass with validate-data fail-closed, not drip-fed.
  • Symmetric-ref drift — same failure mode as fleet↔mission; the migrate script + bidirectional validation is the mitigation (don't hand-author derived member lists).
  • i18n load — program name/summary/outcome are core content → 14 locales each (bounded: dozens of programs, not hundreds).

Orrery — architecture documentation · MIT · No tracking