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
programsschema 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:
- 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.
- 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.
- 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):
| kind | examples |
|---|---|
crewed-campaign | Mercury, Gemini, Apollo, Vostok, Voskhod, Soyuz, Shenzhou, Artemis, Project 921 (China crewed lunar), Gaganyaan |
robotic-campaign | Luna, Zond, Venera, Mariner, Viking, Voyager, Pioneer, Surveyor, Ranger, Chang'e, Tianwen, Chandrayaan, Mars Exploration Program |
station | Salyut, Skylab, Mir, ISS, Tiangong, ROS, BAS, Commercial LEO Destinations |
infrastructure | ILRS, 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):
- 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.
- Goals — what the agency (or coalition) actually set out to do: the specific objectives and the outcomes they were reaching for.
- Outcome — what came of it: goals met, missed, cancelled, redirected, still open. State it; don't grade it.
- Narrative — the story that ties the roster together (the arc, the turning points, the human + engineering stakes). Lightweight editorial, not a textbook.
- 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.
- 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.
- 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/staginglike 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 bybuild-image-provenance.tsintoimage-provenance.json(source, author, license, modifications) and shows up on/creditswith a license summary — thecredits.spec.tsADR-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/stagingapproval; 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 —
/moonfor a lunar program (both the landing-site level, deep-linking the Apollo sites on the Moon map, and the science/surface level),/marsfor Mars programs,/explorefor other bodies; - program-specific
/sciencearticles — 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):
{
"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):
{
"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.tsscript derivesmember_missions[]+member_fleet[]onto each program record (exactly likemigrate-fleet-linked-missions.ts), andvalidate-datachecks bidirectional integrity fail-closed: everyprogramid 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
programstring + a lookup table. Rejected: no home for program metadata, no/programsroute, 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/sciencesub-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. /programsindex — 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/sciencearticle 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:
/programsplaced 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.
- 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.
- Tag all 115 missions + 251 fleet with
program/programs[], populate each program'sroster(link our flagships, name the rest as context). - 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.
- 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/clpsprograms, 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):
- Route — standalone
/programs(recommended) vs a grouping toggle on/missions. funding-linekind — model Discovery / New Frontiers / Great Observatories as programs, or campaigns + stations + infrastructure only?- Membership direction — entry-authored (
programon missions,programs[]on fleet) + derived member lists (recommended, matches repo idiom). rostershape — mandatory or optional? how much prose vs structured linked/context entries?- Nesting — flat/single-primary for v1 (recommended; revisit later).
Risks
- Granularity bikeshedding — "is X a program?" The
kindenum + 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-datafail-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).